This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-io.git
The following commit(s) were added to refs/heads/master by this push: new 218b5e7 Patch from PR 32 from jonfreedman with additional changes and cleanups 218b5e7 is described below commit 218b5e7b7d76654004f4122c674308f78c87954e Author: Gary Gregory <gardgreg...@gmail.com> AuthorDate: Sun Sep 19 11:16:50 2021 -0400 Patch from PR 32 from jonfreedman with additional changes and cleanups - Add Tailable interface to tail files accessed using alternative libraries such as jCIFS or commons-vfs. - Changes to the PR: - Use Objects.requireNonNull() instead manual checks. - Normalize Javadoc. - Add missing Javadoc. - Add missing Javadoc tags. - Sort members. - Don't initialize ivars to defaults. - Renamed some types, ivars, and params. - Fix some Javadoc. - No need to use FQCNs in Javadoc @link. - Use {@code} instead of <code></code>. - YAGNI: Remove some methods that are not tested and never called. - Re-implement some internals using NIO. - Add some constructor parameter validation. - Add a toString() methods. --- src/changes/changes.xml | 7 +- .../java/org/apache/commons/io/input/Tailer.java | 909 +++++++++++++++------ .../org/apache/commons/io/input/TailerTest.java | 319 ++++++-- 3 files changed, 925 insertions(+), 310 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index ed1931c..6b87d8a 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -196,6 +196,9 @@ The <action> type attribute can be add,update,fix,remove. Add and use PathUtils.sizeOfDirectory(Path) Add and use PathUtils.sizeOfDirectoryAsBigInteger(Path) </action> + <action dev="jonfreedman" type="add" due-to="Jon Freedman, Gary Gregory"> + Add Tailer.Tailable interface to allow tailing of remote files for example using jCIFS. + </action> <!-- UPDATE --> <action dev="ggregory" type="add" due-to="Gary Gregory"> Update FileEntry to use FileTime instead of long for file time stamps. @@ -273,7 +276,7 @@ The <action> type attribute can be add,update,fix,remove. </action> <!-- UPDATE --> <action dev="ggregory" type="update" due-to="Dependabot"> - Bump mockito-inline from 3.11.0 to 3.11.2 #247. + Bump mockito-inline from 3.11.0 to 3.11.2 #247. </action> <action dev="ggregory" type="update" due-to="Dependabot"> Bump jmh.version from 1.27 to 1.32 #237. @@ -312,7 +315,7 @@ The <action> type attribute can be add,update,fix,remove. Bump checkstyle from 8.42 to 8.44 #241, #248. </action> <action dev="ggregory" type="update" due-to="Dependabot"> - Bump mockito-inline from 3.10.0 to 3.11.0 #242. + Bump mockito-inline from 3.10.0 to 3.11.0 #242. </action> </release> <release version="2.9.0" date="2021-05-22" description="Java 8 required."> diff --git a/src/main/java/org/apache/commons/io/input/Tailer.java b/src/main/java/org/apache/commons/io/input/Tailer.java index d8ca79c..c881f32 100644 --- a/src/main/java/org/apache/commons/io/input/Tailer.java +++ b/src/main/java/org/apache/commons/io/input/Tailer.java @@ -21,231 +21,628 @@ import static org.apache.commons.io.IOUtils.EOF; import static org.apache.commons.io.IOUtils.LF; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.time.Duration; +import java.util.Arrays; +import java.util.Objects; -import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.apache.commons.io.file.PathUtils; +import org.apache.commons.io.file.attribute.FileTimes; /** - * Simple implementation of the unix "tail -f" functionality. + * Simple implementation of the UNIX "tail -f" functionality. * * <h2>1. Create a TailerListener implementation</h2> * <p> - * First you need to create a {@link TailerListener} implementation - * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to - * implement every method). + * First you need to create a {@link TailerListener} implementation; ({@link TailerListenerAdapter} is provided for + * convenience so that you don't have to implement every method). + * </p> + * + * <p> + * For example: * </p> * - * <p>For example:</p> * <pre> - * public class MyTailerListener extends TailerListenerAdapter { - * public void handle(String line) { - * System.out.println(line); - * } - * }</pre> + * public class MyTailerListener extends TailerListenerAdapter { + * public void handle(String line) { + * System.out.println(line); + * } + * } + * </pre> * * <h2>2. Using a Tailer</h2> * * <p> - * You can create and use a Tailer in one of three ways: + * You can create and use a Tailer in one of four ways: * </p> * <ul> - * <li>Using one of the static helper methods: - * <ul> - * <li>{@link Tailer#create(File, TailerListener)}</li> - * <li>{@link Tailer#create(File, TailerListener, long)}</li> - * <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li> - * </ul> - * </li> - * <li>Using an {@link java.util.concurrent.Executor}</li> - * <li>Using an {@link Thread}</li> + * <li>Using a {@link Builder}</li> + * <li>Using one of the static helper methods: + * <ul> + * <li>{@link Tailer#create(File, TailerListener)}</li> + * <li>{@link Tailer#create(File, TailerListener, long)}</li> + * <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li> + * </ul> + * </li> + * <li>Using an {@link java.util.concurrent.Executor}</li> + * <li>Using a {@link Thread}</li> * </ul> * * <p> - * An example of each of these is shown below. + * An example of each is shown below. * </p> * - * <h3>2.1 Using the static helper method</h3> + * <h3>2.1 Using a Builder</h3> * * <pre> - * TailerListener listener = new MyTailerListener(); - * Tailer tailer = Tailer.create(file, listener, delay);</pre> + * TailerListener listener = new MyTailerListener(); + * Tailer tailer = new Tailer.Builder(file, listener).withDelayDuration(delay).build(); + * </pre> + * + * <h3>2.2 Using the static helper method</h3> + * + * <pre> + * TailerListener listener = new MyTailerListener(); + * Tailer tailer = Tailer.create(file, listener, delay); + * </pre> * - * <h3>2.2 Using an Executor</h3> + * <h3>2.3 Using an Executor</h3> * * <pre> - * TailerListener listener = new MyTailerListener(); - * Tailer tailer = new Tailer(file, listener, delay); + * TailerListener listener = new MyTailerListener(); + * Tailer tailer = new Tailer(file, listener, delay); * - * // stupid executor impl. for demo purposes - * Executor executor = new Executor() { - * public void execute(Runnable command) { - * command.run(); - * } - * }; + * // stupid executor impl. for demo purposes + * Executor executor = new Executor() { + * public void execute(Runnable command) { + * command.run(); + * } + * }; * - * executor.execute(tailer); + * executor.execute(tailer); * </pre> * * - * <h3>2.3 Using a Thread</h3> + * <h3>2.4 Using a Thread</h3> + * * <pre> - * TailerListener listener = new MyTailerListener(); - * Tailer tailer = new Tailer(file, listener, delay); - * Thread thread = new Thread(tailer); - * thread.setDaemon(true); // optional - * thread.start();</pre> + * TailerListener listener = new MyTailerListener(); + * Tailer tailer = new Tailer(file, listener, delay); + * Thread thread = new Thread(tailer); + * thread.setDaemon(true); // optional + * thread.start(); + * </pre> * * <h2>3. Stopping a Tailer</h2> - * <p>Remember to stop the tailer when you have done with it:</p> + * <p> + * Remember to stop the tailer when you have done with it: + * </p> + * * <pre> - * tailer.stop(); + * tailer.stop(); * </pre> * * <h2>4. Interrupting a Tailer</h2> - * <p>You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}. + * <p> + * You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}. * </p> + * * <pre> - * thread.interrupt(); + * thread.interrupt(); * </pre> * <p> * If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}. * </p> * <p> - * The file is read using the default charset; this can be overridden if necessary. + * The file is read using the default Charset; this can be overridden if necessary. * </p> + * * @see TailerListener * @see TailerListenerAdapter * @since 2.0 - * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()} + * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()}. + * @since 2.12.0 Add {@link Tailable} and {@link RandomAccessResourceBridge} interfaces to tail of files accessed using + * alternative libraries such as jCIFS or <a href="https://commons.apache.org/proper/commons-vfs/">Apache Commons + * VFS</a>. */ public class Tailer implements Runnable { - private static final int DEFAULT_DELAY_MILLIS = 1000; + /** + * Builds a {@link Tailer} with default values. + * + * @since 2.12.0 + */ + public static class Builder { + + private final Tailable tailable; + private final TailerListener tailerListener; + private Charset charset = DEFAULT_CHARSET; + private int bufferSize = IOUtils.DEFAULT_BUFFER_SIZE; + private Duration delayDuration = Duration.ofMillis(DEFAULT_DELAY_MILLIS); + private boolean end; + private boolean reOpen; + private boolean startThread = true; + + /** + * Creates a builder. + * + * @param file the file to follow. + * @param listener the TailerListener to use. + */ + public Builder(final File file, final TailerListener listener) { + this(file.toPath(), listener); + } - private static final String RAF_MODE = "r"; + /** + * Creates a builder. + * + * @param file the file to follow. + * @param listener the TailerListener to use. + */ + public Builder(final Path file, final TailerListener listener) { + this(new TailablePath(file), listener); + } - // The default charset used for reading files - private static final Charset DEFAULT_CHARSET = Charset.defaultCharset(); + /** + * Creates a builder. + * + * @param tailable the tailable to follow. + * @param tailerListener the TailerListener to use. + */ + public Builder(final Tailable tailable, final TailerListener tailerListener) { + this.tailable = Objects.requireNonNull(tailable, "tailable"); + this.tailerListener = Objects.requireNonNull(tailerListener, "tailerListener"); + } - /** - * Buffer on top of RandomAccessFile. - */ - private final byte[] inbuf; + /** + * Builds a new configured instance. + * + * @return a new configured instance. + */ + public Tailer build() { + final Tailer tailer = new Tailer(tailable, charset, tailerListener, delayDuration, end, reOpen, bufferSize); + if (startThread) { + final Thread thread = new Thread(tailer); + thread.setDaemon(true); + thread.start(); + } + return tailer; + } - /** - * The file which will be tailed. - */ - private final File file; + /** + * Sets the buffer size. + * + * @param bufferSize Buffer size. + * @return Builder with specific buffer size. + */ + public Builder withBufferSize(final int bufferSize) { + this.bufferSize = bufferSize; + return this; + } - /** - * The character set that will be used to read the file. - */ - private final Charset charset; + /** + * Sets the Charset. + * + * @param charset the Charset to be used for reading the file. + * @return Builder with specific Charset. + */ + public Builder withCharset(final Charset charset) { + this.charset = Objects.requireNonNull(charset, "charset"); + return this; + } + + /** + * Sets the delay duration. + * + * @param delayDuration the delay between checks of the file for new content. + * @return Builder with specific delay duration. + */ + public Builder withDelayDuration(final Duration delayDuration) { + this.delayDuration = Objects.requireNonNull(delayDuration, "delayDuration"); + return this; + } + + /** + * Sets the re-open behavior. + * + * @param reOpen whether to close/reopen the file between chunks + * @return Builder with specific re-open behavior + */ + public Builder withReOpen(final boolean reOpen) { + this.reOpen = reOpen; + return this; + } + + /** + * Sets the daemon thread startup behavior. + * + * @param startThread whether to create a daemon thread automatically. + * @return Builder with specific daemon thread startup behavior. + */ + public Builder withStartThread(final boolean startThread) { + this.startThread = startThread; + return this; + } + + /** + * Sets the tail start behavior. + * + * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. + * @return Builder with specific tail start behavior. + */ + public Builder withTailFromEnd(final boolean end) { + this.end = end; + return this; + } + } /** - * The amount of time to wait for the file to be updated. + * Bridges random access to a {@link RandomAccessFile}. */ - private final Duration delayDuration; + private static final class RandomAccessFileBridge implements RandomAccessResourceBridge { + + private final RandomAccessFile randomAccessFile; + + private RandomAccessFileBridge(final File file, final String mode) throws FileNotFoundException { + randomAccessFile = new RandomAccessFile(file, mode); + } + + @Override + public void close() throws IOException { + randomAccessFile.close(); + } + + @Override + public long getPointer() throws IOException { + return randomAccessFile.getFilePointer(); + } + + @Override + public int read(final byte[] b) throws IOException { + return randomAccessFile.read(b); + } + + @Override + public void seek(final long position) throws IOException { + randomAccessFile.seek(position); + } + + } /** - * Whether to tail from the end or start of file + * Bridges access to a resource for random access, normally a file. Allows substitution of remote files for example + * using jCIFS. + * + * @since 2.12.0 */ - private final boolean end; + public interface RandomAccessResourceBridge extends Closeable { + + /** + * Gets the current offset in this tailable. + * + * @return the offset from the beginning of the tailable, in bytes, at which the next read or write occurs. + * @throws IOException if an I/O error occurs. + */ + long getPointer() throws IOException; + + /** + * Reads up to {@code b.length} bytes of data from this tailable into an array of bytes. This method blocks until at + * least one byte of input is available. + * + * @param b the buffer into which the data is read. + * @return the total number of bytes read into the buffer, or {@code -1} if there is no more data because the end of + * this tailable has been reached. + * @throws IOException If the first byte cannot be read for any reason other than end of tailable, or if the random + * access tailable has been closed, or if some other I/O error occurs. + */ + int read(final byte[] b) throws IOException; + + /** + * Sets the file-pointer offset, measured from the beginning of this tailable, at which the next read or write occurs. + * The offset may be set beyond the end of the tailable. Setting the offset beyond the end of the tailable does not + * change the tailable length. The tailable length will change only by writing after the offset has been set beyond the + * end of the tailable. + * + * @param pos the offset position, measured in bytes from the beginning of the tailable, at which to set the tailable + * pointer. + * @throws IOException if {@code pos} is less than {@code 0} or if an I/O error occurs. + */ + void seek(final long pos) throws IOException; + } /** - * The listener to notify of events when tailing. + * A tailable resource like a file. + * + * @since 2.12.0 */ - private final TailerListener listener; + public interface Tailable { + + /** + * Creates a random access file stream to read from. + * + * @param mode the access mode {@link RandomAccessFile} + * @return a random access file stream to read from + * @throws FileNotFoundException if the tailable object does not exist + */ + RandomAccessResourceBridge getRandomAccess(final String mode) throws FileNotFoundException; + + /** + * Tests if this tailable is newer than the specified {@code FileTime}. + * + * @param fileTime the file time reference. + * @return true if the {@code File} exists and has been modified after the given {@code FileTime}. + * @throws IOException if an I/O error occurs. + */ + boolean isNewer(final FileTime fileTime) throws IOException; + + /** + * Gets the last modification {@link FileTime}. + * + * @return See {@link java.nio.file.Files#getLastModifiedTime(Path, LinkOption...)}. + * @throws IOException if an I/O error occurs. + */ + FileTime lastModifiedFileTime() throws IOException; + + /** + * Gets the size of this tailable. + * + * @return The size, in bytes, of this tailable, or {@code 0} if the file does not exist. Some operating systems may + * return {@code 0} for path names denoting system-dependent entities such as devices or pipes. + * @throws IOException if an I/O error occurs. + */ + long size() throws IOException; + } /** - * Whether to close and reopen the file whilst waiting for more input. + * A tailable for a file {@link Path}. */ - private final boolean reOpen; + private static final class TailablePath implements Tailable { + + private final Path path; + private final LinkOption[] linkOptions; + + private TailablePath(final Path path, final LinkOption... linkOptions) { + this.path = Objects.requireNonNull(path, "path"); + this.linkOptions = linkOptions; + } + + Path getPath() { + return path; + } + + @Override + public RandomAccessResourceBridge getRandomAccess(final String mode) throws FileNotFoundException { + return new RandomAccessFileBridge(path.toFile(), mode); + } + + @Override + public boolean isNewer(final FileTime fileTime) throws IOException { + return PathUtils.isNewer(path, fileTime, linkOptions); + } + + @Override + public FileTime lastModifiedFileTime() throws IOException { + return Files.getLastModifiedTime(path, linkOptions); + } + + @Override + public long size() throws IOException { + return Files.size(path); + } + + @Override + public String toString() { + return "TailablePath [file=" + path + ", linkOptions=" + Arrays.toString(linkOptions) + "]"; + } + } + + private static final int DEFAULT_DELAY_MILLIS = 1000; + + private static final String RAF_READ_ONLY_MODE = "r"; + + // The default charset used for reading files + private static final Charset DEFAULT_CHARSET = Charset.defaultCharset(); /** - * The tailer will run as long as this value is true. + * Creates and starts a Tailer for the given file. + * + * @param file the file to follow. + * @param charset the character set to use for reading the file. + * @param listener the TailerListener to use. + * @param delayMillis the delay between checks of the file for new content in milliseconds. + * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. + * @param reOpen whether to close/reopen the file between chunks. + * @param bufferSize buffer size. + * @return The new tailer. + * @deprecated Use {@link Builder}. */ - private volatile boolean run = true; + @Deprecated + public static Tailer create(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, + final boolean reOpen, final int bufferSize) { + //@formatter:off + return new Builder(file, listener) + .withCharset(charset) + .withDelayDuration(Duration.ofMillis(delayMillis)) + .withTailFromEnd(end) + .withReOpen(reOpen) + .withBufferSize(bufferSize) + .build(); + //@formatter:on + } /** - * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s. - * @param file The file to follow. + * Creates and starts a Tailer for the given file, starting at the beginning of the file with the default delay of 1.0s + * + * @param file the file to follow. * @param listener the TailerListener to use. + * @return The new tailer. + * @deprecated Use {@link Builder}. */ - public Tailer(final File file, final TailerListener listener) { - this(file, listener, DEFAULT_DELAY_MILLIS); + @Deprecated + public static Tailer create(final File file, final TailerListener listener) { + return new Builder(file, listener).build(); } /** - * Creates a Tailer for the given file, starting from the beginning. + * Creates and starts a Tailer for the given file, starting at the beginning of the file + * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. + * @return The new tailer. + * @deprecated Use {@link Builder}. */ - public Tailer(final File file, final TailerListener listener, final long delayMillis) { - this(file, listener, delayMillis, false); + @Deprecated + public static Tailer create(final File file, final TailerListener listener, final long delayMillis) { + //@formatter:off + return new Builder(file, listener) + .withDelayDuration(Duration.ofMillis(delayMillis)) + .build(); + //@formatter:on } /** - * Creates a Tailer for the given file, with a delay other than the default 1.0s. + * Creates and starts a Tailer for the given file with default buffer size. + * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. + * @return The new tailer. + * @deprecated Use {@link Builder}. */ - public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) { - this(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE); + @Deprecated + public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end) { + //@formatter:off + return new Builder(file, listener) + .withDelayDuration(Duration.ofMillis(delayMillis)) + .withTailFromEnd(end) + .build(); + //@formatter:on } /** - * Creates a Tailer for the given file, with a delay other than the default 1.0s. + * Creates and starts a Tailer for the given file with default buffer size. + * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. - * @param reOpen if true, close and reopen the file between reading chunks + * @param reOpen whether to close/reopen the file between chunks. + * @return The new tailer. + * @deprecated Use {@link Builder}. */ - public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, - final boolean reOpen) { - this(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE); + @Deprecated + public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) { + //@formatter:off + return new Builder(file, listener) + .withDelayDuration(Duration.ofMillis(delayMillis)) + .withTailFromEnd(end) + .withReOpen(reOpen) + .build(); + //@formatter:on } /** - * Creates a Tailer for the given file, with a specified buffer size. + * Creates and starts a Tailer for the given file. + * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. - * @param bufSize Buffer size + * @param reOpen whether to close/reopen the file between chunks. + * @param bufferSize buffer size. + * @return The new tailer. + * @deprecated Use {@link Builder}. */ - public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, - final int bufSize) { - this(file, listener, delayMillis, end, false, bufSize); + @Deprecated + public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, + final int bufferSize) { + //@formatter:off + return new Builder(file, listener) + .withDelayDuration(Duration.ofMillis(delayMillis)) + .withTailFromEnd(end) + .withReOpen(reOpen) + .withBufferSize(bufferSize) + .build(); + //@formatter:on } /** - * Creates a Tailer for the given file, with a specified buffer size. + * Creates and starts a Tailer for the given file. + * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. - * @param reOpen if true, close and reopen the file between reading chunks - * @param bufSize Buffer size + * @param bufferSize buffer size. + * @return The new tailer. + * @deprecated Use {@link Builder}. */ - public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, - final boolean reOpen, final int bufSize) { - this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize); + @Deprecated + public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufferSize) { + //@formatter:off + return new Builder(file, listener) + .withDelayDuration(Duration.ofMillis(delayMillis)) + .withTailFromEnd(end) + .withBufferSize(bufferSize) + .build(); + //@formatter:on } /** + * Buffer on top of RandomAccessResourceBridge. + */ + private final byte[] inbuf; + + /** + * The file which will be tailed. + */ + private final Tailable tailable; + + /** + * The character set that will be used to read the file. + */ + private final Charset charset; + + /** + * The amount of time to wait for the file to be updated. + */ + private final Duration delayDuration; + + /** + * Whether to tail from the end or start of file + */ + private final boolean tailAtEnd; + + /** + * The listener to notify of events when tailing. + */ + private final TailerListener listener; + + /** + * Whether to close and reopen the file whilst waiting for more input. + */ + private final boolean reOpen; + + /** + * The tailer will run as long as this value is true. + */ + private volatile boolean run = true; + + /** * Creates a Tailer for the given file, with a specified buffer size. + * * @param file the file to follow. * @param charset the Charset to be used for reading the file * @param listener the TailerListener to use. @@ -253,153 +650,156 @@ public class Tailer implements Runnable { * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen if true, close and reopen the file between reading chunks * @param bufSize Buffer size + * @deprecated Use {@link Builder}. */ - public Tailer(final File file, final Charset charset, final TailerListener listener, final long delayMillis, - final boolean end, final boolean reOpen - , final int bufSize) { - this(file, charset, listener, Duration.ofMillis(delayMillis), end, reOpen, bufSize); + @Deprecated + public Tailer(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, + final int bufSize) { + this(new TailablePath(file.toPath()), charset, listener, Duration.ofMillis(delayMillis), end, reOpen, bufSize); } /** - * Creates a Tailer for the given file, with a specified buffer size. - * @param file the file to follow. - * @param charset the Charset to be used for reading the file + * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s. + * + * @param file The file to follow. * @param listener the TailerListener to use. - * @param delayDuration the delay between checks of the file for new content in milliseconds. - * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. - * @param reOpen if true, close and reopen the file between reading chunks - * @param bufSize Buffer size + * @deprecated Use {@link Builder}. */ - private Tailer(final File file, final Charset charset, final TailerListener listener, final Duration delayDuration, - final boolean end, final boolean reOpen - , final int bufSize) { - this.file = file; - this.delayDuration = delayDuration; - this.end = end; - - this.inbuf = IOUtils.byteArray(bufSize); - - // Save and prepare the listener - this.listener = listener; - listener.init(this); - this.reOpen = reOpen; - this.charset = charset; + @Deprecated + public Tailer(final File file, final TailerListener listener) { + this(file, listener, DEFAULT_DELAY_MILLIS); } /** - * Creates and starts a Tailer for the given file. + * Creates a Tailer for the given file, starting from the beginning. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. - * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. - * @param bufSize buffer size. - * @return The new tailer + * @deprecated Use {@link Builder}. */ - public static Tailer create(final File file, final TailerListener listener, final long delayMillis, - final boolean end, final int bufSize) { - return create(file, listener, delayMillis, end, false, bufSize); + @Deprecated + public Tailer(final File file, final TailerListener listener, final long delayMillis) { + this(file, listener, delayMillis, false); } /** - * Creates and starts a Tailer for the given file. + * Creates a Tailer for the given file, with a delay other than the default 1.0s. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. - * @param reOpen whether to close/reopen the file between chunks - * @param bufSize buffer size. - * @return The new tailer - */ - public static Tailer create(final File file, final TailerListener listener, final long delayMillis, - final boolean end, final boolean reOpen, - final int bufSize) { - return create(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize); + * @deprecated Use {@link Builder}. + */ + @Deprecated + public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) { + this(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE); } /** - * Creates and starts a Tailer for the given file. + * Creates a Tailer for the given file, with a delay other than the default 1.0s. * * @param file the file to follow. - * @param charset the character set to use for reading the file * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. - * @param reOpen whether to close/reopen the file between chunks - * @param bufSize buffer size. - * @return The new tailer - */ - public static Tailer create(final File file, final Charset charset, final TailerListener listener, - final long delayMillis, final boolean end, final boolean reOpen - ,final int bufSize) { - final Tailer tailer = new Tailer(file, charset, listener, delayMillis, end, reOpen, bufSize); - final Thread thread = new Thread(tailer); - thread.setDaemon(true); - thread.start(); - return tailer; + * @param reOpen if true, close and reopen the file between reading chunks + * @deprecated Use {@link Builder}. + */ + @Deprecated + public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) { + this(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE); } /** - * Creates and starts a Tailer for the given file with default buffer size. + * Creates a Tailer for the given file, with a specified buffer size. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. - * @return The new tailer + * @param reOpen if true, close and reopen the file between reading chunks + * @param bufferSize Buffer size + * @deprecated Use {@link Builder}. */ - public static Tailer create(final File file, final TailerListener listener, final long delayMillis, - final boolean end) { - return create(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE); + @Deprecated + public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufferSize) { + this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufferSize); } /** - * Creates and starts a Tailer for the given file with default buffer size. + * Creates a Tailer for the given file, with a specified buffer size. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. - * @param reOpen whether to close/reopen the file between chunks - * @return The new tailer + * @param bufferSize Buffer size + * @deprecated Use {@link Builder}. */ - public static Tailer create(final File file, final TailerListener listener, final long delayMillis, - final boolean end, final boolean reOpen) { - return create(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE); + @Deprecated + public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufferSize) { + this(file, listener, delayMillis, end, false, bufferSize); } /** - * Creates and starts a Tailer for the given file, starting at the beginning of the file + * Creates a Tailer for the given file, with a specified buffer size. * - * @param file the file to follow. + * @param tailable the file to follow. + * @param charset the Charset to be used for reading the file * @param listener the TailerListener to use. - * @param delayMillis the delay between checks of the file for new content in milliseconds. - * @return The new tailer + * @param delayDuration the delay between checks of the file for new content in milliseconds. + * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. + * @param reOpen if true, close and reopen the file between reading chunks + * @param bufferSize Buffer size */ - public static Tailer create(final File file, final TailerListener listener, final long delayMillis) { - return create(file, listener, delayMillis, false); + private Tailer(final Tailable tailable, final Charset charset, final TailerListener listener, final Duration delayDuration, final boolean end, + final boolean reOpen, final int bufferSize) { + this.tailable = tailable; + this.delayDuration = delayDuration; + this.tailAtEnd = end; + this.inbuf = IOUtils.byteArray(bufferSize); + + // Save and prepare the listener + this.listener = listener; + listener.init(this); + this.reOpen = reOpen; + this.charset = charset; } /** - * Creates and starts a Tailer for the given file, starting at the beginning of the file - * with the default delay of 1.0s + * Gets the delay in milliseconds. * - * @param file the file to follow. - * @param listener the TailerListener to use. - * @return The new tailer + * @return the delay in milliseconds. + * @deprecated Use {@link #getDelayDuration()}. */ - public static Tailer create(final File file, final TailerListener listener) { - return create(file, listener, DEFAULT_DELAY_MILLIS, false); + @Deprecated + public long getDelay() { + return delayDuration.toMillis(); + } + + /** + * Gets the delay Duration. + * + * @return the delay Duration. + * @since 2.12.0 + */ + public Duration getDelayDuration() { + return delayDuration; } /** - * Return the file. + * Gets the file. * * @return the file + * @throws IllegalStateException if constructed using a user provided {@link Tailable} implementation */ public File getFile() { - return file; + if (tailable instanceof TailablePath) { + return ((TailablePath) tailable).getPath().toFile(); + } + throw new IllegalStateException("Cannot extract java.io.File from " + tailable.getClass().getName()); } /** @@ -413,37 +813,80 @@ public class Tailer implements Runnable { } /** - * Gets the delay in milliseconds. + * Gets the Tailable. * - * @return the delay in milliseconds. + * @return the Tailable + * @since 2.12.0 */ - public long getDelay() { - return delayDuration.toMillis(); + public Tailable getTailable() { + return tailable; } /** - * Gets the delay Duration. + * Reads new lines. * - * @return the delay Duration. - * @since 2.12.0 + * @param reader The file to read + * @return The new position after the lines have been read + * @throws java.io.IOException if an I/O error occurs. */ - public Duration getDelayDuration() { - return delayDuration; + private long readLines(final RandomAccessResourceBridge reader) throws IOException { + try (ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64)) { + long pos = reader.getPointer(); + long rePos = pos; // position to re-read + int num; + boolean seenCR = false; + while (getRun() && ((num = reader.read(inbuf)) != EOF)) { + for (int i = 0; i < num; i++) { + final byte ch = inbuf[i]; + switch (ch) { + case LF: + seenCR = false; // swallow CR before LF + listener.handle(new String(lineBuf.toByteArray(), charset)); + lineBuf.reset(); + rePos = pos + i + 1; + break; + case CR: + if (seenCR) { + lineBuf.write(CR); + } + seenCR = true; + break; + default: + if (seenCR) { + seenCR = false; // swallow final CR + listener.handle(new String(lineBuf.toByteArray(), charset)); + lineBuf.reset(); + rePos = pos + i + 1; + } + lineBuf.write(ch); + } + } + pos = reader.getPointer(); + } + + reader.seek(rePos); // Ensure we can re-read if necessary + + if (listener instanceof TailerListenerAdapter) { + ((TailerListenerAdapter) listener).endOfFileReached(); + } + + return rePos; + } } /** - * Follows changes in the file, calling the TailerListener's handle method for each new line. + * Follows changes in the file, calling {@link TailerListener#handle(String)} with each new line. */ @Override public void run() { - RandomAccessFile reader = null; + RandomAccessResourceBridge reader = null; try { - FileTime last = FileTime.fromMillis(0); // The last time the file was checked for changes + FileTime last = FileTimes.EPOCH; // The last time the file was checked for changes long position = 0; // position within the file // Open the file while (getRun() && reader == null) { try { - reader = new RandomAccessFile(file, RAF_MODE); + reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE); } catch (final FileNotFoundException e) { listener.fileNotFound(); } @@ -451,22 +894,22 @@ public class Tailer implements Runnable { Thread.sleep(delayDuration.toMillis()); } else { // The current position in the file - position = end ? file.length() : 0; - last = FileUtils.lastModifiedFileTime(file); + position = tailAtEnd ? tailable.size() : 0; + last = tailable.lastModifiedFileTime(); reader.seek(position); } } while (getRun()) { - final boolean newer = FileUtils.isFileNewer(file, last); // IO-279, must be done first + final boolean newer = tailable.isNewer(last); // IO-279, must be done first // Check the file length to see if it was rotated - final long length = file.length(); + final long length = tailable.size(); if (length < position) { // File was rotated listener.fileRotated(); // Reopen the reader after rotation ensuring that the old file is closed iff we re-open it // successfully - try (RandomAccessFile save = reader) { - reader = new RandomAccessFile(file, RAF_MODE); + try (RandomAccessResourceBridge save = reader) { + reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE); // At this point, we're sure that the old file is rotated // Finish scanning the old file and then we'll start with the new one try { @@ -487,25 +930,25 @@ public class Tailer implements Runnable { if (length > position) { // The file has more content than it did last time position = readLines(reader); - last = FileUtils.lastModifiedFileTime(file); + last = tailable.lastModifiedFileTime(); } else if (newer) { /* - * This can happen if the file is truncated or overwritten with the exact same length of - * information. In cases like this, the file position needs to be reset + * This can happen if the file is truncated or overwritten with the exact same length of information. In cases like + * this, the file position needs to be reset */ position = 0; reader.seek(position); // cannot be null here // Now we can read new lines position = readLines(reader); - last = FileUtils.lastModifiedFileTime(file); + last = tailable.lastModifiedFileTime(); } if (reOpen && reader != null) { reader.close(); } Thread.sleep(delayDuration.toMillis()); if (getRun() && reOpen) { - reader = new RandomAccessFile(file, RAF_MODE); + reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE); reader.seek(position); } } @@ -516,9 +959,7 @@ public class Tailer implements Runnable { listener.handle(e); } finally { try { - if (reader != null) { - reader.close(); - } + IOUtils.close(reader); } catch (final IOException e) { listener.handle(e); } @@ -527,61 +968,9 @@ public class Tailer implements Runnable { } /** - * Allows the tailer to complete its current loop and return. + * Requests the tailer to complete its current loop and return. */ public void stop() { this.run = false; } - - /** - * Read new lines. - * - * @param reader The file to read - * @return The new position after the lines have been read - * @throws java.io.IOException if an I/O error occurs. - */ - private long readLines(final RandomAccessFile reader) throws IOException { - try (ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64)) { - long pos = reader.getFilePointer(); - long rePos = pos; // position to re-read - int num; - boolean seenCR = false; - while (getRun() && ((num = reader.read(inbuf)) != EOF)) { - for (int i = 0; i < num; i++) { - final byte ch = inbuf[i]; - switch (ch) { - case LF: - seenCR = false; // swallow CR before LF - listener.handle(new String(lineBuf.toByteArray(), charset)); - lineBuf.reset(); - rePos = pos + i + 1; - break; - case CR: - if (seenCR) { - lineBuf.write(CR); - } - seenCR = true; - break; - default: - if (seenCR) { - seenCR = false; // swallow final CR - listener.handle(new String(lineBuf.toByteArray(), charset)); - lineBuf.reset(); - rePos = pos + i + 1; - } - lineBuf.write(ch); - } - } - pos = reader.getFilePointer(); - } - - reader.seek(rePos); // Ensure we can re-read if necessary - - if (listener instanceof TailerListenerAdapter) { - ((TailerListenerAdapter) listener).endOfFileReached(); - } - - return rePos; - } - } } diff --git a/src/test/java/org/apache/commons/io/input/TailerTest.java b/src/test/java/org/apache/commons/io/input/TailerTest.java index 1e301a1..97f8ea3 100644 --- a/src/test/java/org/apache/commons/io/input/TailerTest.java +++ b/src/test/java/org/apache/commons/io/input/TailerTest.java @@ -36,12 +36,16 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import com.google.common.collect.Lists; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.TestResources; @@ -51,11 +55,66 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; /** - * Tests for {@link Tailer}. - * + * Test for {@link Tailer}. */ public class TailerTest { + private static final int TEST_BUFFER_SIZE = 1024; + + private static final int TEST_DELAY_MILLIS = 1500; + + private static class NonStandardTailable implements Tailer.Tailable { + + private final File file; + + public NonStandardTailable(final File file) { + this.file = file; + } + + @Override + public Tailer.RandomAccessResourceBridge getRandomAccess(final String mode) throws FileNotFoundException { + return new Tailer.RandomAccessResourceBridge() { + + private final RandomAccessFile reader = new RandomAccessFile(file, mode); + + @Override + public void close() throws IOException { + reader.close(); + } + + @Override + public long getPointer() throws IOException { + return reader.getFilePointer(); + } + + @Override + public int read(final byte[] b) throws IOException { + return reader.read(b); + } + + @Override + public void seek(final long position) throws IOException { + reader.seek(position); + } + }; + } + + @Override + public boolean isNewer(final FileTime fileTime) throws IOException { + return FileUtils.isFileNewer(file, fileTime); + } + + @Override + public FileTime lastModifiedFileTime() throws IOException { + return FileUtils.lastModifiedFileTime(file); + } + + @Override + public long size() { + return file.length(); + } + } + /** * Test {@link TailerListener} implementation. */ @@ -64,6 +123,8 @@ public class TailerTest { // Must be synchronized because it is written by one thread and read by another private final List<String> lines = Collections.synchronizedList(new ArrayList<>()); + private final CountDownLatch latch; + volatile Exception exception; volatile int notFound; @@ -74,6 +135,18 @@ public class TailerTest { volatile int reachedEndOfFile; + public TestTailerListener() { + latch = new CountDownLatch(1); + } + + public TestTailerListener(final int expectedLines) { + latch = new CountDownLatch(expectedLines); + } + + public boolean awaitExpectedLines(long timeout, TimeUnit timeUnit) throws InterruptedException { + return latch.await(timeout, timeUnit); + } + public void clear() { lines.clear(); } @@ -105,6 +178,7 @@ public class TailerTest { @Override public void handle(final String line) { lines.add(line); + latch.countDown(); } @Override @@ -118,14 +192,9 @@ public class TailerTest { private Tailer tailer; - protected void createFile(final File file, final long size) - throws IOException { - if (!file.getParentFile().exists()) { - throw new IOException("Cannot create file " + file - + " as the parent directory does not exist"); - } - try (final BufferedOutputStream output = - new BufferedOutputStream(Files.newOutputStream(file.toPath()))) { + protected void createFile(final File file, final long size) throws IOException { + assertTrue(file.getParentFile().exists(), () -> "Cannot create file " + file + " as the parent directory does not exist"); + try (final BufferedOutputStream output = new BufferedOutputStream(Files.newOutputStream(file.toPath()))) { TestUtils.generateTestData(output, size); } @@ -137,19 +206,12 @@ public class TailerTest { try { reader = new RandomAccessFile(file.getPath(), "r"); } catch (final FileNotFoundException ignore) { - } - try { - TestUtils.sleep(200L); - } catch (final InterruptedException ignore) { // ignore } + TestUtils.sleepQuietly(200L); } } finally { - try { - IOUtils.close(reader); - } catch (final IOException ignored) { - // ignored - } + IOUtils.closeQuietly(reader); } } @@ -183,6 +245,79 @@ public class TailerTest { listener.clear(); } + @Test + public void testBuilderWithNonStandardTailable() throws Exception { + final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen-and-buffersize-and-charset.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = new Tailer.Builder(new NonStandardTailable(file), listener).build(); + assertTrue(tailer.getTailable() instanceof NonStandardTailable); + validateTailer(listener, tailer, file); + } + + @Test + public void testCreate() throws Exception { + final File file = new File(temporaryFolder, "tailer-create.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = Tailer.create(file, listener); + validateTailer(listener, tailer, file); + } + + @Test + public void testCreaterWithDelayAndFromStartWithReopen() throws Exception { + final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, false); + validateTailer(listener, tailer, file); + } + + @Test + public void testCreateWithDelay() throws Exception { + final File file = new File(temporaryFolder, "tailer-create-with-delay.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS); + validateTailer(listener, tailer, file); + } + + @Test + public void testCreateWithDelayAndFromStart() throws Exception { + final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false); + validateTailer(listener, tailer, file); + } + + @Test + public void testCreateWithDelayAndFromStartWithBufferSize() throws Exception { + final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-buffersize.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, TEST_BUFFER_SIZE); + validateTailer(listener, tailer, file); + } + + @Test + public void testCreateWithDelayAndFromStartWithReopenAndBufferSize() throws Exception { + final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen-and-buffersize.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE); + validateTailer(listener, tailer, file); + } + + @Test + public void testCreateWithDelayAndFromStartWithReopenAndBufferSizeAndCharset() throws Exception { + final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen-and-buffersize-and-charset.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = Tailer.create(file, StandardCharsets.UTF_8, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE); + validateTailer(listener, tailer, file); + } + /* * Tests [IO-357][Tailer] InterruptedException while the thead is sleeping is silently ignored. */ @@ -280,35 +415,110 @@ public class TailerTest { thread.start(); try (Writer out = new OutputStreamWriter(Files.newOutputStream(file.toPath()), charsetUTF8); - BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(origin.toPath()), charsetUTF8))) { + BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(origin.toPath()), charsetUTF8))) { final List<String> lines = new ArrayList<>(); String line; - while((line = reader.readLine()) != null){ + while ((line = reader.readLine()) != null) { out.write(line); out.write("\n"); lines.add(line); } out.close(); // ensure data is written - final long testDelayMillis = delay * 10; - TestUtils.sleep(testDelayMillis); - final List<String> tailerlines = listener.getLines(); - assertEquals(lines.size(), tailerlines.size(), "line count"); - for(int i = 0,len = lines.size();i<len;i++){ - final String expected = lines.get(i); - final String actual = tailerlines.get(i); - if (!expected.equals(actual)) { - fail("Line: " + i - + "\nExp: (" + expected.length() + ") " + expected - + "\nAct: (" + actual.length() + ") "+ actual); - } - } + final long testDelayMillis = delay * 10; + TestUtils.sleep(testDelayMillis); + final List<String> tailerlines = listener.getLines(); + assertEquals(lines.size(), tailerlines.size(), "line count"); + for (int i = 0, len = lines.size(); i < len; i++) { + final String expected = lines.get(i); + final String actual = tailerlines.get(i); + if (!expected.equals(actual)) { + fail("Line: " + i + "\nExp: (" + expected.length() + ") " + expected + "\nAct: (" + actual.length() + ") " + actual); + } + } } } @Test + public void testSimpleConstructor() throws Exception { + final File file = new File(temporaryFolder, "tailer-simple-constructor.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = new Tailer(file, listener); + final Thread thread = new Thread(tailer); + thread.start(); + validateTailer(listener, tailer, file); + } + + @Test + public void testSimpleConstructorWithDelay() throws Exception { + final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS); + final Thread thread = new Thread(tailer); + thread.start(); + validateTailer(listener, tailer, file); + } + + @Test + public void testSimpleConstructorWithDelayAndFromStart() throws Exception { + final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false); + final Thread thread = new Thread(tailer); + thread.start(); + validateTailer(listener, tailer, file); + } + + @Test + public void testSimpleConstructorWithDelayAndFromStartWithBufferSize() throws Exception { + final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-buffersize.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false, TEST_BUFFER_SIZE); + final Thread thread = new Thread(tailer); + thread.start(); + validateTailer(listener, tailer, file); + } + + @Test + public void testSimpleConstructorWithDelayAndFromStartWithReopen() throws Exception { + final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-reopen.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false, false); + final Thread thread = new Thread(tailer); + thread.start(); + validateTailer(listener, tailer, file); + } + + @Test + public void testSimpleConstructorWithDelayAndFromStartWithReopenAndBufferSize() throws Exception { + final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-reopen-and-buffersize.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE); + final Thread thread = new Thread(tailer); + thread.start(); + validateTailer(listener, tailer, file); + } + + @Test + public void testSimpleConstructorWithDelayAndFromStartWithReopenAndBufferSizeAndCharset() throws Exception { + final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-reopen-and-buffersize-and-charset.txt"); + createFile(file, 0); + final TestTailerListener listener = new TestTailerListener(1); + final Tailer tailer = new Tailer(file, StandardCharsets.UTF_8, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE); + final Thread thread = new Thread(tailer); + thread.start(); + validateTailer(listener, tailer, file); + } + + @Test public void testStopWithNoFile() throws Exception { - final File file = new File(temporaryFolder,"nosuchfile"); + final File file = new File(temporaryFolder, "nosuchfile"); assertFalse(file.exists(), "nosuchfile should not exist"); final TestTailerListener listener = new TestTailerListener(); final int delay = 100; @@ -316,17 +526,17 @@ public class TailerTest { tailer = Tailer.create(file, listener, delay, false); TestUtils.sleep(idle); tailer.stop(); - TestUtils.sleep(delay+idle); + TestUtils.sleep(delay + idle); assertNull(listener.exception, "Should not generate Exception"); - assertEquals(1 , listener.initialized, "Expected init to be called"); + assertEquals(1, listener.initialized, "Expected init to be called"); assertTrue(listener.notFound > 0, "fileNotFound should be called"); - assertEquals(0 , listener.rotated, "fileRotated should be not be called"); + assertEquals(0, listener.rotated, "fileRotated should be not be called"); assertEquals(0, listener.reachedEndOfFile, "end of file never reached"); } @Test public void testStopWithNoFileUsingExecutor() throws Exception { - final File file = new File(temporaryFolder,"nosuchfile"); + final File file = new File(temporaryFolder, "nosuchfile"); assertFalse(file.exists(), "nosuchfile should not exist"); final TestTailerListener listener = new TestTailerListener(); final int delay = 100; @@ -336,11 +546,11 @@ public class TailerTest { exec.execute(tailer); TestUtils.sleep(idle); tailer.stop(); - TestUtils.sleep(delay+idle); + TestUtils.sleep(delay + idle); assertNull(listener.exception, "Should not generate Exception"); - assertEquals(1 , listener.initialized, "Expected init to be called"); + assertEquals(1, listener.initialized, "Expected init to be called"); assertTrue(listener.notFound > 0, "fileNotFound should be called"); - assertEquals(0 , listener.rotated, "fileRotated should be not be called"); + assertEquals(0, listener.rotated, "fileRotated should be not be called"); assertEquals(0, listener.reachedEndOfFile, "end of file never reached"); } @@ -405,9 +615,10 @@ public class TailerTest { assertEquals(0, listener.getLines().size(), "4 line count"); assertNotNull(listener.exception, "Missing InterruptedException"); assertTrue(listener.exception instanceof InterruptedException, "Unexpected Exception: " + listener.exception); - assertEquals(1 , listener.initialized, "Expected init to be called"); - // assertEquals(0 , listener.notFound, "fileNotFound should not be called"); // there is a window when it might be called - assertEquals(1 , listener.rotated, "fileRotated should be be called"); + assertEquals(1, listener.initialized, "Expected init to be called"); + // assertEquals(0 , listener.notFound, "fileNotFound should not be called"); // there is a window when it might be + // called + assertEquals(1, listener.rotated, "fileRotated should be be called"); } @Test @@ -468,7 +679,19 @@ public class TailerTest { listener.clear(); } - /** Append some lines to a file */ + private void validateTailer(final TestTailerListener listener, final Tailer tailer, final File file) throws Exception { + try { + write(file, "foo"); + final int timeout = 30; + final TimeUnit timeoutUnit = TimeUnit.SECONDS; + assertTrue(listener.awaitExpectedLines(timeout, timeoutUnit), () -> String.format("await timed out after %s %s", timeout, timeoutUnit)); + assertEquals(listener.getLines(), Lists.newArrayList("foo"), "lines"); + } finally { + tailer.stop(); + } + } + + /** Appends lines to a file */ private void write(final File file, final String... lines) throws Exception { try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardOpenOption.APPEND)) { for (final String line : lines) { @@ -477,8 +700,8 @@ public class TailerTest { } } - /** Append a string to a file */ - private void writeString(final File file, final String ... strings) throws Exception { + /** Appends strings to a file */ + private void writeString(final File file, final String... strings) throws Exception { try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardOpenOption.APPEND)) { for (final String string : strings) { writer.write(string);