    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     */
    017    package org.apache.commons.io.input;
    018
    019    import java.io.File;
    020    import java.io.FileNotFoundException;
    021    import java.io.IOException;
    022    import java.io.RandomAccessFile;
    023
    024    import org.apache.commons.io.FileUtils;
    025    import org.apache.commons.io.IOUtils;
    026
    027    /**
    028     * Simple implementation of the unix "tail -f" functionality.
    029     * <p>
    030     * <h2>1. Create a TailerListener implementation</h3>
    031     * <p>
    032     * First you need to create a {@link TailerListener} implementation
    033     * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to
    034     * implement every method).
    035     * </p>
    036     *
    037     * <p>For example:</p>
    038     * <pre>
    039     *  public class MyTailerListener extends TailerListenerAdapter {
    040     *      public void handle(String line) {
    041     *          System.out.println(line);
    042     *      }
    043     *  }
    044     * </pre>
    045     *
    046     * <h2>2. Using a Tailer</h2>
    047     *
    048     * You can create and use a Tailer in one of three ways:
    049     * <ul>
    050     *   <li>Using one of the static helper methods:
    051     *     <ul>
    052     *       <li>{@link Tailer#create(File, TailerListener)}</li>
    053     *       <li>{@link Tailer#create(File, TailerListener, long)}</li>
    054     *       <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li>
    055     *     </ul>
    056     *   </li>
    057     *   <li>Using an {@link java.util.concurrent.Executor}</li>
    058     *   <li>Using an {@link Thread}</li>
    059     * </ul>
    060     *
    061     * An example of each of these is shown below.
    062     *
    063     * <h3>2.1 Using the static helper method</h3>
    064     *
    065     * <pre>
    066     *      TailerListener listener = new MyTailerListener();
    067     *      Tailer tailer = Tailer.create(file, listener, delay);
    068     * </pre>
    069     *
    070     * <h3>2.2 Use an Executor</h3>
    071     *
    072     * <pre>
    073     *      TailerListener listener = new MyTailerListener();
    074     *      Tailer tailer = new Tailer(file, listener, delay);
    075     *
    076     *      // stupid executor impl. for demo purposes
    077     *      Executor executor = new Executor() {
    078     *          public void execute(Runnable command) {
    079     *              command.run();
    080     *           }
    081     *      };
    082     *
    083     *      executor.execute(tailer);
    084     * </pre>
    085     *
    086     *
    087     * <h3>2.3 Use a Thread</h3>
    088     * <pre>
    089     *      TailerListener listener = new MyTailerListener();
    090     *      Tailer tailer = new Tailer(file, listener, delay);
    091     *      Thread thread = new Thread(tailer);
    092     *      thread.setDaemon(true); // optional
    093     *      thread.start();
    094     * </pre>
    095     *
    096     * <h2>3. Stop Tailing</h3>
    097     * <p>Remember to stop the tailer when you have done with it:</p>
    098     * <pre>
    099     *      tailer.stop();
    100     * </pre>
    101     *
    102     * @see TailerListener
    103     * @see TailerListenerAdapter
    104     * @version $Id: Tailer.java 1033786 2010-11-11 00:37:37Z niallp $
    105     * @since Commons IO 2.0
    106     */
    107    public class Tailer implements Runnable {
    108
    109        /**
    110         * The file which will be tailed.
    111         */
    112        private final File file;
    113
    114        /**
    115         * The amount of time to wait for the file to be updated.
    116         */
    117        private final long delay;
    118
    119        /**
    120         * Whether to tail from the end or start of file
    121         */
    122        private final boolean end;
    123
    124        /**
    125         * The listener to notify of events when tailing.
    126         */
    127        private final TailerListener listener;
    128
    129        /**
    130         * The tailer will run as long as this value is true.
    131         */
    132        private volatile boolean run = true;
    133
    134        /**
    135         * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
    136         * @param file The file to follow.
    137         * @param listener the TailerListener to use.
    138         */
    139        public Tailer(File file, TailerListener listener) {
    140            this(file, listener, 1000);
    141        }
    142
    143        /**
    144         * Creates a Tailer for the given file, starting from the beginning.
    145         * @param file the file to follow.
    146         * @param listener the TailerListener to use.
    147         * @param delay the delay between checks of the file for new content in milliseconds.
    148         */
    149        public Tailer(File file, TailerListener listener, long delay) {
    150            this(file, listener, 1000, false);
    151        }
    152
    153        /**
    154         * Creates a Tailer for the given file, with a delay other than the default 1.0s.
    155         * @param file the file to follow.
    156         * @param listener the TailerListener to use.
    157         * @param delay the delay between checks of the file for new content in milliseconds.
    158         * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    159         */
    160        public Tailer(File file, TailerListener listener, long delay, boolean end) {
    161
    162            this.file = file;
    163            this.delay = delay;
    164            this.end = end;
    165
    166            // Save and prepare the listener
    167            this.listener = listener;
    168            listener.init(this);
    169        }
    170
    171        /**
    172         * Creates and starts a Tailer for the given file.
    173         *
    174         * @param file the file to follow.
    175         * @param listener the TailerListener to use.
    176         * @param delay the delay between checks of the file for new content in milliseconds.
    177         * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    178         * @return The new tailer
    179         */
    180        public static Tailer create(File file, TailerListener listener, long delay, boolean end) {
    181            Tailer tailer = new Tailer(file, listener, delay, end);
    182            Thread thread = new Thread(tailer);
    183            thread.setDaemon(true);
    184            thread.start();
    185            return tailer;
    186        }
    187
    188        /**
    189         * Creates and starts a Tailer for the given file, starting at the beginning of the file
    190         *
    191         * @param file the file to follow.
    192         * @param listener the TailerListener to use.
    193         * @param delay the delay between checks of the file for new content in milliseconds.
    194         * @return The new tailer
    195         */
    196        public static Tailer create(File file, TailerListener listener, long delay) {
    197            return create(file, listener, delay, false);
    198        }
    199
    200        /**
    201         * Creates and starts a Tailer for the given file, starting at the beginning of the file
    202         * with the default delay of 1.0s
    203         *
    204         * @param file the file to follow.
    205         * @param listener the TailerListener to use.
    206         * @return The new tailer
    207         */
    208        public static Tailer create(File file, TailerListener listener) {
    209            return create(file, listener, 1000, false);
    210        }
    211
    212        /**
    213         * Return the file.
    214         *
    215         * @return the file
    216         */
    217        public File getFile() {
    218            return file;
    219        }
    220
    221        /**
    222         * Return the delay.
    223         *
    224         * @return the delay
    225         */
    226        public long getDelay() {
    227            return delay;
    228        }
    229
    230        /**
    231         * Follows changes in the file, calling the TailerListener's handle method for each new line.
    232         */
    233        public void run() {
    234            RandomAccessFile reader = null;
    235            try {
    236                long last = 0; // The last time the file was checked for changes
    237                long position = 0; // position within the file
    238                // Open the file
    239                while (run && reader == null) {
    240                    try {
    241                        reader = new RandomAccessFile(file, "r");
    242                    } catch (FileNotFoundException e) {
    243                        listener.fileNotFound();
    244                    }
    245
    246                    if (reader == null) {
    247                        try {
    248                            Thread.sleep(delay);
    249                        } catch (InterruptedException e) {
    250                        }
    251                    } else {
    252                        // The current position in the file
    253                        position = end ? file.length() : 0;
    254                        last = System.currentTimeMillis();
    255                        reader.seek(position);
    256                    }
    257                }
    258
    259
    260                while (run) {
    261
    262                    // Check the file length to see if it was rotated
    263                    long length = file.length();
    264
    265                    if (length < position) {
    266
    267                        // File was rotated
    268                        listener.fileRotated();
    269
    270                        // Reopen the reader after rotation
    271                        try {
    272                            // Ensure that the old file is closed iff we re-open it successfully
    273                            RandomAccessFile save = reader;
    274                            reader = new RandomAccessFile(file, "r");
    275                            position = 0;
    276                            // close old file explicitly rather than relying on GC picking up previous RAF
    277                            IOUtils.closeQuietly(save);
    278                        } catch (FileNotFoundException e) {
    279                            // in this case we continue to use the previous reader and position values
    280                            listener.fileNotFound();
    281                        }
    282                        continue;
    283                    } else {
    284
    285                        // File was not rotated
    286
    287                        // See if the file needs to be read again
    288                        if (length > position) {
    289
    290                            // The file has more content than it did last time
    291                            last = System.currentTimeMillis();
    292                            position = readLines(reader);
    293
    294                        } else if (FileUtils.isFileNewer(file, last)) {
    295
    296                            /* This can happen if the file is truncated or overwritten
    297                             * with the exact same length of information. In cases like
    298                             * this, the file position needs to be reset
    299                             */
    300                            position = 0;
    301                            reader.seek(position); // cannot be null here
    302
    303                            // Now we can read new lines
    304                            last = System.currentTimeMillis();
    305                            position = readLines(reader);
    306                        }
    307                    }
    308                    try {
    309                        Thread.sleep(delay);
    310                    } catch (InterruptedException e) {
    311                    }
    312                }
    313
    314            } catch (Exception e) {
    315
    316                listener.handle(e);
    317
    318            } finally {
    319                IOUtils.closeQuietly(reader);
    320            }
    321        }
    322
    323        /**
    324         * Allows the tailer to complete its current loop and return.
    325         */
    326        public void stop() {
    327            this.run = false;
    328        }
    329
    330        /**
    331         * Read new lines.
    332         *
    333         * @param reader The file to read
    334         * @return The new position after the lines have been read
    335         * @throws java.io.IOException if an I/O error occurs.
    336         */
    337        private long readLines(RandomAccessFile reader) throws IOException {
    338            String line = reader.readLine();
    339            while (line != null) {
    340                listener.handle(line);
    341                line = reader.readLine();
    342            }
    343            return reader.getFilePointer();
    344        }
    345
    346    }