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);