    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.io.input;
    018
    019import static org.apache.commons.io.IOUtils.CR;
    020import static org.apache.commons.io.IOUtils.EOF;
    021import static org.apache.commons.io.IOUtils.LF;
    022
    023import java.io.ByteArrayOutputStream;
    024import java.io.Closeable;
    025import java.io.File;
    026import java.io.FileNotFoundException;
    027import java.io.IOException;
    028import java.io.RandomAccessFile;
    029import java.nio.charset.Charset;
    030import java.nio.file.Files;
    031import java.nio.file.LinkOption;
    032import java.nio.file.Path;
    033import java.nio.file.attribute.FileTime;
    034import java.time.Duration;
    035import java.util.Arrays;
    036import java.util.Objects;
    037import java.util.concurrent.ExecutorService;
    038import java.util.concurrent.Executors;
    039
    040import org.apache.commons.io.IOUtils;
    041import org.apache.commons.io.ThreadUtils;
    042import org.apache.commons.io.build.AbstractOrigin;
    043import org.apache.commons.io.build.AbstractStreamBuilder;
    044import org.apache.commons.io.file.PathUtils;
    045import org.apache.commons.io.file.attribute.FileTimes;
    046
    047/**
    048 * Simple implementation of the UNIX "tail -f" functionality.
    049 *
    050 * <h2>1. Create a TailerListener implementation</h2>
    051 * <p>
    052 * First you need to create a {@link TailerListener} implementation; ({@link TailerListenerAdapter} is provided for
    053 * convenience so that you don't have to implement every method).
    054 * </p>
    055 *
    056 * <p>
    057 * For example:
    058 * </p>
    059 *
    060 * <pre>
    061 * public class MyTailerListener extends TailerListenerAdapter {
    062 *     public void handle(String line) {
    063 *         System.out.println(line);
    064 *     }
    065 * }
    066 * </pre>
    067 *
    068 * <h2>2. Using a Tailer</h2>
    069 *
    070 * <p>
    071 * You can create and use a Tailer in one of three ways:
    072 * </p>
    073 * <ul>
    074 * <li>Using a {@link Builder}</li>
    075 * <li>Using an {@link java.util.concurrent.Executor}</li>
    076 * <li>Using a {@link Thread}</li>
    077 * </ul>
    078 *
    079 * <p>
    080 * An example of each is shown below.
    081 * </p>
    082 *
    083 * <h3>2.1 Using a Builder</h3>
    084 *
    085 * <pre>
    086 * TailerListener listener = new MyTailerListener();
    087 * Tailer tailer = Tailer.builder()
    088 *   .setFile(file)
    089 *   .setTailerListener(listener)
    090 *   .setDelayDuration(delay)
    091 *   .get();
    092 * </pre>
    093 *
    094 * <h3>2.2 Using an Executor</h3>
    095 *
    096 * <pre>
    097 * TailerListener listener = new MyTailerListener();
    098 * Tailer tailer = new Tailer(file, listener, delay);
    099 *
    100 * // stupid executor impl. for demo purposes
    101 * Executor executor = new Executor() {
    102 *     public void execute(Runnable command) {
    103 *         command.run();
    104 *     }
    105 * };
    106 *
    107 * executor.execute(tailer);
    108 * </pre>
    109 *
    110 *
    111 * <h3>2.3 Using a Thread</h3>
    112 *
    113 * <pre>
    114 * TailerListener listener = new MyTailerListener();
    115 * Tailer tailer = new Tailer(file, listener, delay);
    116 * Thread thread = new Thread(tailer);
    117 * thread.setDaemon(true); // optional
    118 * thread.start();
    119 * </pre>
    120 *
    121 * <h2>3. Stopping a Tailer</h2>
    122 * <p>
    123 * Remember to stop the tailer when you have done with it:
    124 * </p>
    125 *
    126 * <pre>
    127 * tailer.stop();
    128 * </pre>
    129 *
    130 * <h2>4. Interrupting a Tailer</h2>
    131 * <p>
    132 * You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}.
    133 * </p>
    134 *
    135 * <pre>
    136 * thread.interrupt();
    137 * </pre>
    138 * <p>
    139 * If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}.
    140 * </p>
    141 * <p>
    142 * The file is read using the default Charset; this can be overridden if necessary.
    143 * </p>
    144 *
    145 * @see TailerListener
    146 * @see TailerListenerAdapter
    147 * @since 2.0
    148 * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()}.
    149 * @since 2.12.0 Add {@link Tailable} and {@link RandomAccessResourceBridge} interfaces to tail of files accessed using
    150 *        alternative libraries such as jCIFS or <a href="https://commons.apache.org/proper/commons-vfs/">Apache Commons
    151 *        VFS</a>.
    152 */
    153public class Tailer implements Runnable, AutoCloseable {
    154
    155    /**
    156     * Builds a {@link Tailer} with default values.
    157     * <p>
    158     * For example:
    159     * </p>
    160     * <pre>{@code
    161     * Tailer t = Tailer.builder()
    162     *   .setPath(path)
    163     *   .setCharset(StandardCharsets.UTF_8)
    164     *   .setDelayDuration(Duration.ofSeconds(1))
    165     *   .setExecutorService(Executors.newSingleThreadExecutor(Builder::newDaemonThread))
    166     *   .setReOpen(false)
    167     *   .setStartThread(true)
    168     *   .setTailable(tailable)
    169     *   .setTailerListener(tailerListener)
    170     *   .setTailFromEnd(false)
    171     *   .get()}
    172     * </pre>
    173     * <p>
    174     * @since 2.12.0
    175     */
    176    public static class Builder extends AbstractStreamBuilder<Tailer, Builder> {
    177
    178        private static final Duration DEFAULT_DELAY_DURATION = Duration.ofMillis(DEFAULT_DELAY_MILLIS);
    179
    180        /**
    181         * Creates a new daemon thread.
    182         *
    183         * @param runnable the thread's runnable.
    184         * @return a new daemon thread.
    185         */
    186        private static Thread newDaemonThread(final Runnable runnable) {
    187            final Thread thread = new Thread(runnable, "commons-io-tailer");
    188            thread.setDaemon(true);
    189            return thread;
    190        }
    191
    192        private Tailable tailable;
    193        private TailerListener tailerListener;
    194        private Duration delayDuration = DEFAULT_DELAY_DURATION;
    195        private boolean end;
    196        private boolean reOpen;
    197        private boolean startThread = true;
    198        private ExecutorService executorService = Executors.newSingleThreadExecutor(Builder::newDaemonThread);
    199
    200        /**
    201         * Builds and starts a new configured instance.
    202         *
    203         * The tailer is started if {@code startThread} is true.
    204         *
    205         * @return a new configured instance.
    206         */
    207        @Override
    208        public Tailer get() {
    209            final Tailer tailer = new Tailer(tailable, getCharset(), tailerListener, delayDuration, end, reOpen, getBufferSize());
    210            if (startThread) {
    211                executorService.submit(tailer);
    212            }
    213            return tailer;
    214        }
    215
    216        /**
    217         * Sets the delay duration. null resets to the default delay of one second.
    218         *
    219         * @param delayDuration the delay between checks of the file for new content.
    220         * @return this
    221         */
    222        public Builder setDelayDuration(final Duration delayDuration) {
    223            this.delayDuration = delayDuration != null ? delayDuration : DEFAULT_DELAY_DURATION;
    224            return this;
    225        }
    226
    227        /**
    228         * Sets the executor service to use when startThread is true.
    229         *
    230         * @param executorService the executor service to use when startThread is true.
    231         * @return this
    232         */
    233        public Builder setExecutorService(final ExecutorService executorService) {
    234            this.executorService = Objects.requireNonNull(executorService, "executorService");
    235            return this;
    236        }
    237
    238        /**
    239         * Sets the origin.
    240         *
    241         * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
    242         */
    243        @Override
    244        protected Builder setOrigin(final AbstractOrigin<?, ?> origin) {
    245            setTailable(new TailablePath(origin.getPath()));
    246            return super.setOrigin(origin);
    247        }
    248
    249        /**
    250         * Sets the re-open behavior.
    251         *
    252         * @param reOpen whether to close/reopen the file between chunks
    253         * @return this
    254         */
    255        public Builder setReOpen(final boolean reOpen) {
    256            this.reOpen = reOpen;
    257            return this;
    258        }
    259
    260        /**
    261         * Sets the daemon thread startup behavior.
    262         *
    263         * @param startThread whether to create a daemon thread automatically.
    264         * @return this
    265         */
    266        public Builder setStartThread(final boolean startThread) {
    267            this.startThread = startThread;
    268            return this;
    269        }
    270
    271        /**
    272         * Sets the tailable.
    273         *
    274         * @param tailable the tailable.
    275         * @return this.
    276         */
    277        public Builder setTailable(final Tailable tailable) {
    278            this.tailable = Objects.requireNonNull(tailable, "tailable");
    279            return this;
    280        }
    281
    282        /**
    283         * Sets the listener.
    284         *
    285         * @param tailerListener the listener.
    286         * @return this
    287         */
    288        public Builder setTailerListener(final TailerListener tailerListener) {
    289            this.tailerListener = Objects.requireNonNull(tailerListener, "tailerListener");
    290            return this;
    291        }
    292
    293        /**
    294         * Sets the tail start behavior.
    295         *
    296         * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    297         * @return this
    298         */
    299        public Builder setTailFromEnd(final boolean end) {
    300            this.end = end;
    301            return this;
    302        }
    303    }
    304
    305    /**
    306     * Bridges random access to a {@link RandomAccessFile}.
    307     */
    308    private static final class RandomAccessFileBridge implements RandomAccessResourceBridge {
    309
    310        private final RandomAccessFile randomAccessFile;
    311
    312        private RandomAccessFileBridge(final File file, final String mode) throws FileNotFoundException {
    313            randomAccessFile = new RandomAccessFile(file, mode);
    314        }
    315
    316        @Override
    317        public void close() throws IOException {
    318            randomAccessFile.close();
    319        }
    320
    321        @Override
    322        public long getPointer() throws IOException {
    323            return randomAccessFile.getFilePointer();
    324        }
    325
    326        @Override
    327        public int read(final byte[] b) throws IOException {
    328            return randomAccessFile.read(b);
    329        }
    330
    331        @Override
    332        public void seek(final long position) throws IOException {
    333            randomAccessFile.seek(position);
    334        }
    335
    336    }
    337
    338    /**
    339     * Bridges access to a resource for random access, normally a file. Allows substitution of remote files for example
    340     * using jCIFS.
    341     *
    342     * @since 2.12.0
    343     */
    344    public interface RandomAccessResourceBridge extends Closeable {
    345
    346        /**
    347         * Gets the current offset in this tailable.
    348         *
    349         * @return the offset from the beginning of the tailable, in bytes, at which the next read or write occurs.
    350         * @throws IOException if an I/O error occurs.
    351         */
    352        long getPointer() throws IOException;
    353
    354        /**
    355         * Reads up to {@code b.length} bytes of data from this tailable into an array of bytes. This method blocks until at
    356         * least one byte of input is available.
    357         *
    358         * @param b the buffer into which the data is read.
    359         * @return the total number of bytes read into the buffer, or {@code -1} if there is no more data because the end of
    360         *         this tailable has been reached.
    361         * @throws IOException If the first byte cannot be read for any reason other than end of tailable, or if the random
    362         *         access tailable has been closed, or if some other I/O error occurs.
    363         */
    364        int read(final byte[] b) throws IOException;
    365
    366        /**
    367         * Sets the file-pointer offset, measured from the beginning of this tailable, at which the next read or write occurs.
    368         * The offset may be set beyond the end of the tailable. Setting the offset beyond the end of the tailable does not
    369         * change the tailable length. The tailable length will change only by writing after the offset has been set beyond the
    370         * end of the tailable.
    371         *
    372         * @param pos the offset position, measured in bytes from the beginning of the tailable, at which to set the tailable
    373         *        pointer.
    374         * @throws IOException if {@code pos} is less than {@code 0} or if an I/O error occurs.
    375         */
    376        void seek(final long pos) throws IOException;
    377    }
    378
    379    /**
    380     * A tailable resource like a file.
    381     *
    382     * @since 2.12.0
    383     */
    384    public interface Tailable {
    385
    386        /**
    387         * Creates a random access file stream to read.
    388         *
    389         * @param mode the access mode, by default this is for {@link RandomAccessFile}.
    390         * @return a random access file stream to read.
    391         * @throws FileNotFoundException if the tailable object does not exist.
    392         */
    393        RandomAccessResourceBridge getRandomAccess(final String mode) throws FileNotFoundException;
    394
    395        /**
    396         * Tests if this tailable is newer than the specified {@link FileTime}.
    397         *
    398         * @param fileTime the file time reference.
    399         * @return true if the {@link File} exists and has been modified after the given {@link FileTime}.
    400         * @throws IOException if an I/O error occurs.
    401         */
    402        boolean isNewer(final FileTime fileTime) throws IOException;
    403
    404        /**
    405         * Gets the last modification {@link FileTime}.
    406         *
    407         * @return See {@link java.nio.file.Files#getLastModifiedTime(Path, LinkOption...)}.
    408         * @throws IOException if an I/O error occurs.
    409         */
    410        FileTime lastModifiedFileTime() throws IOException;
    411
    412        /**
    413         * Gets the size of this tailable.
    414         *
    415         * @return The size, in bytes, of this tailable, or {@code 0} if the file does not exist. Some operating systems may
    416         *         return {@code 0} for path names denoting system-dependent entities such as devices or pipes.
    417         * @throws IOException if an I/O error occurs.
    418         */
    419        long size() throws IOException;
    420    }
    421
    422    /**
    423     * A tailable for a file {@link Path}.
    424     */
    425    private static final class TailablePath implements Tailable {
    426
    427        private final Path path;
    428        private final LinkOption[] linkOptions;
    429
    430        private TailablePath(final Path path, final LinkOption... linkOptions) {
    431            this.path = Objects.requireNonNull(path, "path");
    432            this.linkOptions = linkOptions;
    433        }
    434
    435        Path getPath() {
    436            return path;
    437        }
    438
    439        @Override
    440        public RandomAccessResourceBridge getRandomAccess(final String mode) throws FileNotFoundException {
    441            return new RandomAccessFileBridge(path.toFile(), mode);
    442        }
    443
    444        @Override
    445        public boolean isNewer(final FileTime fileTime) throws IOException {
    446            return PathUtils.isNewer(path, fileTime, linkOptions);
    447        }
    448
    449        @Override
    450        public FileTime lastModifiedFileTime() throws IOException {
    451            return Files.getLastModifiedTime(path, linkOptions);
    452        }
    453
    454        @Override
    455        public long size() throws IOException {
    456            return Files.size(path);
    457        }
    458
    459        @Override
    460        public String toString() {
    461            return "TailablePath [file=" + path + ", linkOptions=" + Arrays.toString(linkOptions) + "]";
    462        }
    463    }
    464
    465    private static final int DEFAULT_DELAY_MILLIS = 1000;
    466
    467    private static final String RAF_READ_ONLY_MODE = "r";
    468
    469    // The default charset used for reading files
    470    private static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
    471
    472    /**
    473     * Constructs a new {@link Builder}.
    474     *
    475     * @return Creates a new {@link Builder}.
    476     * @since 2.12.0
    477     */
    478    public static Builder builder() {
    479        return new Builder();
    480    }
    481
    482    /**
    483     * Creates and starts a Tailer for the given file.
    484     *
    485     * @param file the file to follow.
    486     * @param charset the character set to use for reading the file.
    487     * @param listener the TailerListener to use.
    488     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    489     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    490     * @param reOpen whether to close/reopen the file between chunks.
    491     * @param bufferSize buffer size.
    492     * @return The new tailer.
    493     * @deprecated Use {@link #builder()}.
    494     */
    495    @Deprecated
    496    public static Tailer create(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end,
    497        final boolean reOpen, final int bufferSize) {
    498        //@formatter:off
    499        return builder()
    500                .setFile(file)
    501                .setTailerListener(listener)
    502                .setCharset(charset)
    503                .setDelayDuration(Duration.ofMillis(delayMillis))
    504                .setTailFromEnd(end)
    505                .setReOpen(reOpen)
    506                .setBufferSize(bufferSize)
    507                .get();
    508        //@formatter:on
    509    }
    510
    511    /**
    512     * Creates and starts a Tailer for the given file, starting at the beginning of the file with the default delay of 1.0s
    513     *
    514     * @param file the file to follow.
    515     * @param listener the TailerListener to use.
    516     * @return The new tailer.
    517     * @deprecated Use {@link #builder()}.
    518     */
    519    @Deprecated
    520    public static Tailer create(final File file, final TailerListener listener) {
    521        //@formatter:off
    522        return builder()
    523                .setFile(file)
    524                .setTailerListener(listener)
    525                .get();
    526        //@formatter:on
    527    }
    528
    529    /**
    530     * Creates and starts a Tailer for the given file, starting at the beginning of the file
    531     *
    532     * @param file the file to follow.
    533     * @param listener the TailerListener to use.
    534     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    535     * @return The new tailer.
    536     * @deprecated Use {@link #builder()}.
    537     */
    538    @Deprecated
    539    public static Tailer create(final File file, final TailerListener listener, final long delayMillis) {
    540        //@formatter:off
    541        return builder()
    542                .setFile(file)
    543                .setTailerListener(listener)
    544                .setDelayDuration(Duration.ofMillis(delayMillis))
    545                .get();
    546        //@formatter:on
    547    }
    548
    549    /**
    550     * Creates and starts a Tailer for the given file with default buffer size.
    551     *
    552     * @param file the file to follow.
    553     * @param listener the TailerListener to use.
    554     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    555     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    556     * @return The new tailer.
    557     * @deprecated Use {@link #builder()}.
    558     */
    559    @Deprecated
    560    public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
    561        //@formatter:off
    562        return builder()
    563                .setFile(file)
    564                .setTailerListener(listener)
    565                .setDelayDuration(Duration.ofMillis(delayMillis))
    566                .setTailFromEnd(end)
    567                .get();
    568        //@formatter:on
    569    }
    570
    571    /**
    572     * Creates and starts a Tailer for the given file with default buffer size.
    573     *
    574     * @param file the file to follow.
    575     * @param listener the TailerListener to use.
    576     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    577     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    578     * @param reOpen whether to close/reopen the file between chunks.
    579     * @return The new tailer.
    580     * @deprecated Use {@link #builder()}.
    581     */
    582    @Deprecated
    583    public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) {
    584        //@formatter:off
    585        return builder()
    586                .setFile(file)
    587                .setTailerListener(listener)
    588                .setDelayDuration(Duration.ofMillis(delayMillis))
    589                .setTailFromEnd(end)
    590                .setReOpen(reOpen)
    591                .get();
    592        //@formatter:on
    593    }
    594
    595    /**
    596     * Creates and starts a Tailer for the given file.
    597     *
    598     * @param file the file to follow.
    599     * @param listener the TailerListener to use.
    600     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    601     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    602     * @param reOpen whether to close/reopen the file between chunks.
    603     * @param bufferSize buffer size.
    604     * @return The new tailer.
    605     * @deprecated Use {@link #builder()}.
    606     */
    607    @Deprecated
    608    public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen,
    609        final int bufferSize) {
    610        //@formatter:off
    611        return builder()
    612                .setFile(file)
    613                .setTailerListener(listener)
    614                .setDelayDuration(Duration.ofMillis(delayMillis))
    615                .setTailFromEnd(end)
    616                .setReOpen(reOpen)
    617                .setBufferSize(bufferSize)
    618                .get();
    619        //@formatter:on
    620    }
    621
    622    /**
    623     * Creates and starts a Tailer for the given file.
    624     *
    625     * @param file the file to follow.
    626     * @param listener the TailerListener to use.
    627     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    628     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    629     * @param bufferSize buffer size.
    630     * @return The new tailer.
    631     * @deprecated Use {@link #builder()}.
    632     */
    633    @Deprecated
    634    public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufferSize) {
    635        //@formatter:off
    636        return builder()
    637                .setFile(file)
    638                .setTailerListener(listener)
    639                .setDelayDuration(Duration.ofMillis(delayMillis))
    640                .setTailFromEnd(end)
    641                .setBufferSize(bufferSize)
    642                .get();
    643        //@formatter:on
    644    }
    645
    646    /**
    647     * Buffer on top of RandomAccessResourceBridge.
    648     */
    649    private final byte[] inbuf;
    650
    651    /**
    652     * The file which will be tailed.
    653     */
    654    private final Tailable tailable;
    655
    656    /**
    657     * The character set that will be used to read the file.
    658     */
    659    private final Charset charset;
    660
    661    /**
    662     * The amount of time to wait for the file to be updated.
    663     */
    664    private final Duration delayDuration;
    665
    666    /**
    667     * Whether to tail from the end or start of file
    668     */
    669    private final boolean tailAtEnd;
    670
    671    /**
    672     * The listener to notify of events when tailing.
    673     */
    674    private final TailerListener listener;
    675
    676    /**
    677     * Whether to close and reopen the file whilst waiting for more input.
    678     */
    679    private final boolean reOpen;
    680
    681    /**
    682     * The tailer will run as long as this value is true.
    683     */
    684    private volatile boolean run = true;
    685
    686    /**
    687     * Creates a Tailer for the given file, with a specified buffer size.
    688     *
    689     * @param file the file to follow.
    690     * @param charset the Charset to be used for reading the file
    691     * @param listener the TailerListener to use.
    692     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    693     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    694     * @param reOpen if true, close and reopen the file between reading chunks
    695     * @param bufSize Buffer size
    696     * @deprecated Use {@link #builder()}.
    697     */
    698    @Deprecated
    699    public Tailer(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen,
    700        final int bufSize) {
    701        this(new TailablePath(file.toPath()), charset, listener, Duration.ofMillis(delayMillis), end, reOpen, bufSize);
    702    }
    703
    704    /**
    705     * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
    706     *
    707     * @param file The file to follow.
    708     * @param listener the TailerListener to use.
    709     * @deprecated Use {@link #builder()}.
    710     */
    711    @Deprecated
    712    public Tailer(final File file, final TailerListener listener) {
    713        this(file, listener, DEFAULT_DELAY_MILLIS);
    714    }
    715
    716    /**
    717     * Creates a Tailer for the given file, starting from the beginning.
    718     *
    719     * @param file the file to follow.
    720     * @param listener the TailerListener to use.
    721     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    722     * @deprecated Use {@link #builder()}.
    723     */
    724    @Deprecated
    725    public Tailer(final File file, final TailerListener listener, final long delayMillis) {
    726        this(file, listener, delayMillis, false);
    727    }
    728
    729    /**
    730     * Creates a Tailer for the given file, with a delay other than the default 1.0s.
    731     *
    732     * @param file the file to follow.
    733     * @param listener the TailerListener to use.
    734     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    735     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    736     * @deprecated Use {@link #builder()}.
    737     */
    738    @Deprecated
    739    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
    740        this(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE);
    741    }
    742
    743    /**
    744     * Creates a Tailer for the given file, with a delay other than the default 1.0s.
    745     *
    746     * @param file the file to follow.
    747     * @param listener the TailerListener to use.
    748     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    749     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    750     * @param reOpen if true, close and reopen the file between reading chunks
    751     * @deprecated Use {@link #builder()}.
    752     */
    753    @Deprecated
    754    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) {
    755        this(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE);
    756    }
    757
    758    /**
    759     * Creates a Tailer for the given file, with a specified buffer size.
    760     *
    761     * @param file the file to follow.
    762     * @param listener the TailerListener to use.
    763     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    764     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    765     * @param reOpen if true, close and reopen the file between reading chunks
    766     * @param bufferSize Buffer size
    767     * @deprecated Use {@link #builder()}.
    768     */
    769    @Deprecated
    770    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufferSize) {
    771        this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufferSize);
    772    }
    773
    774    /**
    775     * Creates a Tailer for the given file, with a specified buffer size.
    776     *
    777     * @param file the file to follow.
    778     * @param listener the TailerListener to use.
    779     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    780     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    781     * @param bufferSize Buffer size
    782     * @deprecated Use {@link #builder()}.
    783     */
    784    @Deprecated
    785    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufferSize) {
    786        this(file, listener, delayMillis, end, false, bufferSize);
    787    }
    788
    789    /**
    790     * Creates a Tailer for the given file, with a specified buffer size.
    791     *
    792     * @param tailable the file to follow.
    793     * @param charset the Charset to be used for reading the file
    794     * @param listener the TailerListener to use.
    795     * @param delayDuration the delay between checks of the file for new content in milliseconds.
    796     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    797     * @param reOpen if true, close and reopen the file between reading chunks
    798     * @param bufferSize Buffer size
    799     */
    800    private Tailer(final Tailable tailable, final Charset charset, final TailerListener listener, final Duration delayDuration, final boolean end,
    801        final boolean reOpen, final int bufferSize) {
    802        this.tailable = Objects.requireNonNull(tailable, "tailable");
    803        this.listener = Objects.requireNonNull(listener, "listener");
    804        this.delayDuration = delayDuration;
    805        this.tailAtEnd = end;
    806        this.inbuf = IOUtils.byteArray(bufferSize);
    807
    808        // Save and prepare the listener
    809        listener.init(this);
    810        this.reOpen = reOpen;
    811        this.charset = charset;
    812    }
    813
    814    /**
    815     * Requests the tailer to complete its current loop and return.
    816     */
    817    @Override
    818    public void close() {
    819        this.run = false;
    820    }
    821
    822    /**
    823     * Gets the delay in milliseconds.
    824     *
    825     * @return the delay in milliseconds.
    826     * @deprecated Use {@link #getDelayDuration()}.
    827     */
    828    @Deprecated
    829    public long getDelay() {
    830        return delayDuration.toMillis();
    831    }
    832
    833    /**
    834     * Gets the delay Duration.
    835     *
    836     * @return the delay Duration.
    837     * @since 2.12.0
    838     */
    839    public Duration getDelayDuration() {
    840        return delayDuration;
    841    }
    842
    843    /**
    844     * Gets the file.
    845     *
    846     * @return the file
    847     * @throws IllegalStateException if constructed using a user provided {@link Tailable} implementation
    848     */
    849    public File getFile() {
    850        if (tailable instanceof TailablePath) {
    851            return ((TailablePath) tailable).getPath().toFile();
    852        }
    853        throw new IllegalStateException("Cannot extract java.io.File from " + tailable.getClass().getName());
    854    }
    855
    856    /**
    857     * Gets whether to keep on running.
    858     *
    859     * @return whether to keep on running.
    860     * @since 2.5
    861     */
    862    protected boolean getRun() {
    863        return run;
    864    }
    865
    866    /**
    867     * Gets the Tailable.
    868     *
    869     * @return the Tailable
    870     * @since 2.12.0
    871     */
    872    public Tailable getTailable() {
    873        return tailable;
    874    }
    875
    876    /**
    877     * Reads new lines.
    878     *
    879     * @param reader The file to read
    880     * @return The new position after the lines have been read
    881     * @throws IOException if an I/O error occurs.
    882     */
    883    private long readLines(final RandomAccessResourceBridge reader) throws IOException {
    884        try (ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64)) {
    885            long pos = reader.getPointer();
    886            long rePos = pos; // position to re-read
    887            int num;
    888            boolean seenCR = false;
    889            while (getRun() && (num = reader.read(inbuf)) != EOF) {
    890                for (int i = 0; i < num; i++) {
    891                    final byte ch = inbuf[i];
    892                    switch (ch) {
    893                    case LF:
    894                        seenCR = false; // swallow CR before LF
    895                        listener.handle(new String(lineBuf.toByteArray(), charset));
    896                        lineBuf.reset();
    897                        rePos = pos + i + 1;
    898                        break;
    899                    case CR:
    900                        if (seenCR) {
    901                            lineBuf.write(CR);
    902                        }
    903                        seenCR = true;
    904                        break;
    905                    default:
    906                        if (seenCR) {
    907                            seenCR = false; // swallow final CR
    908                            listener.handle(new String(lineBuf.toByteArray(), charset));
    909                            lineBuf.reset();
    910                            rePos = pos + i + 1;
    911                        }
    912                        lineBuf.write(ch);
    913                    }
    914                }
    915                pos = reader.getPointer();
    916            }
    917
    918            reader.seek(rePos); // Ensure we can re-read if necessary
    919
    920            if (listener instanceof TailerListenerAdapter) {
    921                ((TailerListenerAdapter) listener).endOfFileReached();
    922            }
    923
    924            return rePos;
    925        }
    926    }
    927
    928    /**
    929     * Follows changes in the file, calling {@link TailerListener#handle(String)} with each new line.
    930     */
    931    @Override
    932    public void run() {
    933        RandomAccessResourceBridge reader = null;
    934        try {
    935            FileTime last = FileTimes.EPOCH; // The last time the file was checked for changes
    936            long position = 0; // position within the file
    937            // Open the file
    938            while (getRun() && reader == null) {
    939                try {
    940                    reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE);
    941                } catch (final FileNotFoundException e) {
    942                    listener.fileNotFound();
    943                }
    944                if (reader == null) {
    945                    ThreadUtils.sleep(delayDuration);
    946                } else {
    947                    // The current position in the file
    948                    position = tailAtEnd ? tailable.size() : 0;
    949                    last = tailable.lastModifiedFileTime();
    950                    reader.seek(position);
    951                }
    952            }
    953            while (getRun()) {
    954                final boolean newer = tailable.isNewer(last); // IO-279, must be done first
    955                // Check the file length to see if it was rotated
    956                final long length = tailable.size();
    957                if (length < position) {
    958                    // File was rotated
    959                    listener.fileRotated();
    960                    // Reopen the reader after rotation ensuring that the old file is closed iff we re-open it
    961                    // successfully
    962                    try (RandomAccessResourceBridge save = reader) {
    963                        reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE);
    964                        // At this point, we're sure that the old file is rotated
    965                        // Finish scanning the old file and then we'll start with the new one
    966                        try {
    967                            readLines(save);
    968                        } catch (final IOException ioe) {
    969                            listener.handle(ioe);
    970                        }
    971                        position = 0;
    972                    } catch (final FileNotFoundException e) {
    973                        // in this case we continue to use the previous reader and position values
    974                        listener.fileNotFound();
    975                        ThreadUtils.sleep(delayDuration);
    976                    }
    977                    continue;
    978                }
    979                // File was not rotated
    980                // See if the file needs to be read again
    981                if (length > position) {
    982                    // The file has more content than it did last time
    983                    position = readLines(reader);
    984                    last = tailable.lastModifiedFileTime();
    985                } else if (newer) {
    986                    /*
    987                     * This can happen if the file is truncated or overwritten with the exact same length of information. In cases like
    988                     * this, the file position needs to be reset
    989                     */
    990                    position = 0;
    991                    reader.seek(position); // cannot be null here
    992
    993                    // Now we can read new lines
    994                    position = readLines(reader);
    995                    last = tailable.lastModifiedFileTime();
    996                }
    997                if (reOpen && reader != null) {
    998                    reader.close();
    999                }
    1000                ThreadUtils.sleep(delayDuration);
    1001                if (getRun() && reOpen) {
    1002                    reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE);
    1003                    reader.seek(position);
    1004                }
    1005            }
    1006        } catch (final InterruptedException e) {
    1007            Thread.currentThread().interrupt();
    1008            listener.handle(e);
    1009        } catch (final Exception e) {
    1010            listener.handle(e);
    1011        } finally {
    1012            try {
    1013                IOUtils.close(reader);
    1014            } catch (final IOException e) {
    1015                listener.handle(e);
    1016            }
    1017            close();
    1018        }
    1019    }
    1020
    1021    /**
    1022     * Requests the tailer to complete its current loop and return.
    1023     *
    1024     * @deprecated Use {@link #close()}.
    1025     */
    1026    @Deprecated
    1027    public void stop() {
    1028        close();
    1029    }
    1030}