You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2021/09/19 15:16:54 UTC
[commons-io] branch master updated: Patch from PR 32 from
jonfreedman with additional changes and cleanups
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 <ga...@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);