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