    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.File;
    025import java.io.FileNotFoundException;
    026import java.io.IOException;
    027import java.io.RandomAccessFile;
    028import java.nio.charset.Charset;
    029
    030import org.apache.commons.io.FileUtils;
    031import org.apache.commons.io.IOUtils;
    032
    033/**
    034 * Simple implementation of the unix "tail -f" functionality.
    035 *
    036 * <h2>1. Create a TailerListener implementation</h2>
    037 * <p>
    038 * First you need to create a {@link TailerListener} implementation
    039 * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to
    040 * implement every method).
    041 * </p>
    042 *
    043 * <p>For example:</p>
    044 * <pre>
    045 *  public class MyTailerListener extends TailerListenerAdapter {
    046 *      public void handle(String line) {
    047 *          System.out.println(line);
    048 *      }
    049 *  }</pre>
    050 *
    051 * <h2>2. Using a Tailer</h2>
    052 *
    053 * <p>
    054 * You can create and use a Tailer in one of three ways:
    055 * </p>
    056 * <ul>
    057 *   <li>Using one of the static helper methods:
    058 *     <ul>
    059 *       <li>{@link Tailer#create(File, TailerListener)}</li>
    060 *       <li>{@link Tailer#create(File, TailerListener, long)}</li>
    061 *       <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li>
    062 *     </ul>
    063 *   </li>
    064 *   <li>Using an {@link java.util.concurrent.Executor}</li>
    065 *   <li>Using an {@link Thread}</li>
    066 * </ul>
    067 *
    068 * <p>
    069 * An example of each of these is shown below.
    070 * </p>
    071 *
    072 * <h3>2.1 Using the static helper method</h3>
    073 *
    074 * <pre>
    075 *      TailerListener listener = new MyTailerListener();
    076 *      Tailer tailer = Tailer.create(file, listener, delay);</pre>
    077 *
    078 * <h3>2.2 Using an Executor</h3>
    079 *
    080 * <pre>
    081 *      TailerListener listener = new MyTailerListener();
    082 *      Tailer tailer = new Tailer(file, listener, delay);
    083 *
    084 *      // stupid executor impl. for demo purposes
    085 *      Executor executor = new Executor() {
    086 *          public void execute(Runnable command) {
    087 *              command.run();
    088 *           }
    089 *      };
    090 *
    091 *      executor.execute(tailer);
    092 * </pre>
    093 *
    094 *
    095 * <h3>2.3 Using a Thread</h3>
    096 * <pre>
    097 *      TailerListener listener = new MyTailerListener();
    098 *      Tailer tailer = new Tailer(file, listener, delay);
    099 *      Thread thread = new Thread(tailer);
    100 *      thread.setDaemon(true); // optional
    101 *      thread.start();</pre>
    102 *
    103 * <h2>3. Stopping a Tailer</h2>
    104 * <p>Remember to stop the tailer when you have done with it:</p>
    105 * <pre>
    106 *      tailer.stop();
    107 * </pre>
    108 *
    109 * <h2>4. Interrupting a Tailer</h2>
    110 * <p>You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}.
    111 * </p>
    112 * <pre>
    113 *      thread.interrupt();
    114 * </pre>
    115 * <p>
    116 * If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}.
    117 * </p>
    118 * <p>
    119 * The file is read using the default charset; this can be overridden if necessary.
    120 * </p>
    121 * @see TailerListener
    122 * @see TailerListenerAdapter
    123 * @since 2.0
    124 * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()}
    125 */
    126public class Tailer implements Runnable {
    127
    128    private static final int DEFAULT_DELAY_MILLIS = 1000;
    129
    130    private static final String RAF_MODE = "r";
    131
    132    // The default charset used for reading files
    133    private static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
    134
    135    /**
    136     * Buffer on top of RandomAccessFile.
    137     */
    138    private final byte[] inbuf;
    139
    140    /**
    141     * The file which will be tailed.
    142     */
    143    private final File file;
    144
    145    /**
    146     * The character set that will be used to read the file.
    147     */
    148    private final Charset charset;
    149
    150    /**
    151     * The amount of time to wait for the file to be updated.
    152     */
    153    private final long delayMillis;
    154
    155    /**
    156     * Whether to tail from the end or start of file
    157     */
    158    private final boolean end;
    159
    160    /**
    161     * The listener to notify of events when tailing.
    162     */
    163    private final TailerListener listener;
    164
    165    /**
    166     * Whether to close and reopen the file whilst waiting for more input.
    167     */
    168    private final boolean reOpen;
    169
    170    /**
    171     * The tailer will run as long as this value is true.
    172     */
    173    private volatile boolean run = true;
    174
    175    /**
    176     * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
    177     * @param file The file to follow.
    178     * @param listener the TailerListener to use.
    179     */
    180    public Tailer(final File file, final TailerListener listener) {
    181        this(file, listener, DEFAULT_DELAY_MILLIS);
    182    }
    183
    184    /**
    185     * Creates a Tailer for the given file, starting from the beginning.
    186     * @param file the file to follow.
    187     * @param listener the TailerListener to use.
    188     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    189     */
    190    public Tailer(final File file, final TailerListener listener, final long delayMillis) {
    191        this(file, listener, delayMillis, false);
    192    }
    193
    194    /**
    195     * Creates a Tailer for the given file, with a delay other than the default 1.0s.
    196     * @param file the file to follow.
    197     * @param listener the TailerListener to use.
    198     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    199     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    200     */
    201    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
    202        this(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE);
    203    }
    204
    205    /**
    206     * Creates a Tailer for the given file, with a delay other than the default 1.0s.
    207     * @param file the file to follow.
    208     * @param listener the TailerListener to use.
    209     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    210     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    211     * @param reOpen if true, close and reopen the file between reading chunks
    212     */
    213    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end,
    214                  final boolean reOpen) {
    215        this(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE);
    216    }
    217
    218    /**
    219     * Creates a Tailer for the given file, with a specified buffer size.
    220     * @param file the file to follow.
    221     * @param listener the TailerListener to use.
    222     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    223     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    224     * @param bufSize Buffer size
    225     */
    226    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end,
    227                  final int bufSize) {
    228        this(file, listener, delayMillis, end, false, bufSize);
    229    }
    230
    231    /**
    232     * Creates a Tailer for the given file, with a specified buffer size.
    233     * @param file the file to follow.
    234     * @param listener the TailerListener to use.
    235     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    236     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    237     * @param reOpen if true, close and reopen the file between reading chunks
    238     * @param bufSize Buffer size
    239     */
    240    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end,
    241                  final boolean reOpen, final int bufSize) {
    242        this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize);
    243    }
    244
    245    /**
    246     * Creates a Tailer for the given file, with a specified buffer size.
    247     * @param file the file to follow.
    248     * @param charset the Charset to be used for reading the file
    249     * @param listener the TailerListener to use.
    250     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    251     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    252     * @param reOpen if true, close and reopen the file between reading chunks
    253     * @param bufSize Buffer size
    254     */
    255    public Tailer(final File file, final Charset charset, final TailerListener listener, final long delayMillis,
    256                  final boolean end, final boolean reOpen
    257            , final int bufSize) {
    258        this.file = file;
    259        this.delayMillis = delayMillis;
    260        this.end = end;
    261
    262        this.inbuf = IOUtils.byteArray(bufSize);
    263
    264        // Save and prepare the listener
    265        this.listener = listener;
    266        listener.init(this);
    267        this.reOpen = reOpen;
    268        this.charset = charset;
    269    }
    270
    271    /**
    272     * Creates and starts a Tailer for the given file.
    273     *
    274     * @param file the file to follow.
    275     * @param listener the TailerListener to use.
    276     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    277     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    278     * @param bufSize buffer size.
    279     * @return The new tailer
    280     */
    281    public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
    282                                final boolean end, final int bufSize) {
    283        return create(file, listener, delayMillis, end, false, bufSize);
    284    }
    285
    286    /**
    287     * Creates and starts a Tailer for the given file.
    288     *
    289     * @param file the file to follow.
    290     * @param listener the TailerListener to use.
    291     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    292     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    293     * @param reOpen whether to close/reopen the file between chunks
    294     * @param bufSize buffer size.
    295     * @return The new tailer
    296     */
    297    public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
    298                                final boolean end, final boolean reOpen,
    299            final int bufSize) {
    300        return create(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize);
    301    }
    302
    303    /**
    304     * Creates and starts a Tailer for the given file.
    305     *
    306     * @param file the file to follow.
    307     * @param charset the character set to use for reading the file
    308     * @param listener the TailerListener to use.
    309     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    310     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    311     * @param reOpen whether to close/reopen the file between chunks
    312     * @param bufSize buffer size.
    313     * @return The new tailer
    314     */
    315    public static Tailer create(final File file, final Charset charset, final TailerListener listener,
    316                                final long delayMillis, final boolean end, final boolean reOpen
    317            ,final int bufSize) {
    318        final Tailer tailer = new Tailer(file, charset, listener, delayMillis, end, reOpen, bufSize);
    319        final Thread thread = new Thread(tailer);
    320        thread.setDaemon(true);
    321        thread.start();
    322        return tailer;
    323    }
    324
    325    /**
    326     * Creates and starts a Tailer for the given file with default buffer size.
    327     *
    328     * @param file the file to follow.
    329     * @param listener the TailerListener to use.
    330     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    331     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    332     * @return The new tailer
    333     */
    334    public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
    335                                final boolean end) {
    336        return create(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE);
    337    }
    338
    339    /**
    340     * Creates and starts a Tailer for the given file with default buffer size.
    341     *
    342     * @param file the file to follow.
    343     * @param listener the TailerListener to use.
    344     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    345     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    346     * @param reOpen whether to close/reopen the file between chunks
    347     * @return The new tailer
    348     */
    349    public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
    350                                final boolean end, final boolean reOpen) {
    351        return create(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE);
    352    }
    353
    354    /**
    355     * Creates and starts a Tailer for the given file, starting at the beginning of the file
    356     *
    357     * @param file the file to follow.
    358     * @param listener the TailerListener to use.
    359     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    360     * @return The new tailer
    361     */
    362    public static Tailer create(final File file, final TailerListener listener, final long delayMillis) {
    363        return create(file, listener, delayMillis, false);
    364    }
    365
    366    /**
    367     * Creates and starts a Tailer for the given file, starting at the beginning of the file
    368     * with the default delay of 1.0s
    369     *
    370     * @param file the file to follow.
    371     * @param listener the TailerListener to use.
    372     * @return The new tailer
    373     */
    374    public static Tailer create(final File file, final TailerListener listener) {
    375        return create(file, listener, DEFAULT_DELAY_MILLIS, false);
    376    }
    377
    378    /**
    379     * Return the file.
    380     *
    381     * @return the file
    382     */
    383    public File getFile() {
    384        return file;
    385    }
    386
    387    /**
    388     * Gets whether to keep on running.
    389     *
    390     * @return whether to keep on running.
    391     * @since 2.5
    392     */
    393    protected boolean getRun() {
    394        return run;
    395    }
    396
    397    /**
    398     * Return the delay in milliseconds.
    399     *
    400     * @return the delay in milliseconds.
    401     */
    402    public long getDelay() {
    403        return delayMillis;
    404    }
    405
    406    /**
    407     * Follows changes in the file, calling the TailerListener's handle method for each new line.
    408     */
    409    @Override
    410    public void run() {
    411        RandomAccessFile reader = null;
    412        try {
    413            long last = 0; // The last time the file was checked for changes
    414            long position = 0; // position within the file
    415            // Open the file
    416            while (getRun() && reader == null) {
    417                try {
    418                    reader = new RandomAccessFile(file, RAF_MODE);
    419                } catch (final FileNotFoundException e) {
    420                    listener.fileNotFound();
    421                }
    422                if (reader == null) {
    423                    Thread.sleep(delayMillis);
    424                } else {
    425                    // The current position in the file
    426                    position = end ? file.length() : 0;
    427                    last = FileUtils.lastModified(file);
    428                    reader.seek(position);
    429                }
    430            }
    431            while (getRun()) {
    432                final boolean newer = FileUtils.isFileNewer(file, last); // IO-279, must be done first
    433                // Check the file length to see if it was rotated
    434                final long length = file.length();
    435                if (length < position) {
    436                    // File was rotated
    437                    listener.fileRotated();
    438                    // Reopen the reader after rotation ensuring that the old file is closed iff we re-open it
    439                    // successfully
    440                    try (RandomAccessFile save = reader) {
    441                        reader = new RandomAccessFile(file, RAF_MODE);
    442                        // At this point, we're sure that the old file is rotated
    443                        // Finish scanning the old file and then we'll start with the new one
    444                        try {
    445                            readLines(save);
    446                        } catch (final IOException ioe) {
    447                            listener.handle(ioe);
    448                        }
    449                        position = 0;
    450                    } catch (final FileNotFoundException e) {
    451                        // in this case we continue to use the previous reader and position values
    452                        listener.fileNotFound();
    453                        Thread.sleep(delayMillis);
    454                    }
    455                    continue;
    456                }
    457                // File was not rotated
    458                // See if the file needs to be read again
    459                if (length > position) {
    460                    // The file has more content than it did last time
    461                    position = readLines(reader);
    462                    last = FileUtils.lastModified(file);
    463                } else if (newer) {
    464                    /*
    465                     * This can happen if the file is truncated or overwritten with the exact same length of
    466                     * information. In cases like this, the file position needs to be reset
    467                     */
    468                    position = 0;
    469                    reader.seek(position); // cannot be null here
    470
    471                    // Now we can read new lines
    472                    position = readLines(reader);
    473                    last = FileUtils.lastModified(file);
    474                }
    475                if (reOpen && reader != null) {
    476                    reader.close();
    477                }
    478                Thread.sleep(delayMillis);
    479                if (getRun() && reOpen) {
    480                    reader = new RandomAccessFile(file, RAF_MODE);
    481                    reader.seek(position);
    482                }
    483            }
    484        } catch (final InterruptedException e) {
    485            Thread.currentThread().interrupt();
    486            listener.handle(e);
    487        } catch (final Exception e) {
    488            listener.handle(e);
    489        } finally {
    490            try {
    491                if (reader != null) {
    492                    reader.close();
    493                }
    494            } catch (final IOException e) {
    495                listener.handle(e);
    496            }
    497            stop();
    498        }
    499    }
    500
    501    /**
    502     * Allows the tailer to complete its current loop and return.
    503     */
    504    public void stop() {
    505        this.run = false;
    506    }
    507
    508    /**
    509     * Read new lines.
    510     *
    511     * @param reader The file to read
    512     * @return The new position after the lines have been read
    513     * @throws java.io.IOException if an I/O error occurs.
    514     */
    515    private long readLines(final RandomAccessFile reader) throws IOException {
    516        try (ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64)) {
    517            long pos = reader.getFilePointer();
    518            long rePos = pos; // position to re-read
    519            int num;
    520            boolean seenCR = false;
    521            while (getRun() && ((num = reader.read(inbuf)) != EOF)) {
    522                for (int i = 0; i < num; i++) {
    523                    final byte ch = inbuf[i];
    524                    switch (ch) {
    525                    case LF:
    526                        seenCR = false; // swallow CR before LF
    527                        listener.handle(new String(lineBuf.toByteArray(), charset));
    528                        lineBuf.reset();
    529                        rePos = pos + i + 1;
    530                        break;
    531                    case CR:
    532                        if (seenCR) {
    533                            lineBuf.write(CR);
    534                        }
    535                        seenCR = true;
    536                        break;
    537                    default:
    538                        if (seenCR) {
    539                            seenCR = false; // swallow final CR
    540                            listener.handle(new String(lineBuf.toByteArray(), charset));
    541                            lineBuf.reset();
    542                            rePos = pos + i + 1;
    543                        }
    544                        lineBuf.write(ch);
    545                    }
    546                }
    547                pos = reader.getFilePointer();
    548            }
    549
    550            reader.seek(rePos); // Ensure we can re-read if necessary
    551
    552            if (listener instanceof TailerListenerAdapter) {
    553                ((TailerListenerAdapter) listener).endOfFileReached();
    554            }
    555
    556            return rePos;
    557        }
    558    }
    559}