    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     * Example Usage:
    031     * <pre>
    032     *      // Simplest invocation using static helper method:
    033     *      TailerListener listener = ...
    034     *      Tailer tailer = Tailer.create(file, listener, delay);
    035     *
    036     *      // Alternative method using executor:
    037     *      TailerListener listener = ...
    038     *      Tailer tailer = new Tailer(file, listener, delay);
    039     *      Executor executor ...
    040     *      executor.execute(tailer);
    041     *
    042     *      // Alternative method if you want to handle the threading yourself:
    043     *      TailerListener listener = ...
    044     *      Tailer tailer = new Tailer(file, listener, delay);
    045     *      Thread thread = new Thread(tailer);
    046     *      thread.setDaemon(true); // optional
    047     *      thread.start();
    048     *
    049     *      // Remember to stop the tailer when you have done with it:
    050     *      tailer.stop();
    051     * </pre>
    052     *
    053     * @version $Id: Tailer.java 1021884 2010-10-12 18:49:16Z ggregory $
    054     * @since Commons IO 2.0
    055     */
    056    public class Tailer implements Runnable {
    057
    058        /**
    059         * The file which will be tailed.
    060         */
    061        private final File file;
    062
    063        /**
    064         * The amount of time to wait for the file to be updated.
    065         */
    066        private final long delay;
    067
    068        /**
    069         * Whether to tail from the end or start of file
    070         */
    071        private final boolean end;
    072
    073        /**
    074         * The listener to notify of events when tailing.
    075         */
    076        private final TailerListener listener;
    077
    078        /**
    079         * The tailer will run as long as this value is true.
    080         */
    081        private volatile boolean run = true;
    082
    083        /**
    084         * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
    085         * @param file The file to follow.
    086         * @param listener the TailerListener to use.
    087         */
    088        public Tailer(File file, TailerListener listener) {
    089            this(file, listener, 1000);
    090        }
    091
    092        /**
    093         * Creates a Tailer for the given file, starting from the beginning.
    094         * @param file the file to follow.
    095         * @param listener the TailerListener to use.
    096         * @param delay the delay between checks of the file for new content in milliseconds.
    097         */
    098        public Tailer(File file, TailerListener listener, long delay) {
    099            this(file, listener, 1000, false);
    100        }
    101
    102        /**
    103         * Creates a Tailer for the given file, with a delay other than the default 1.0s.
    104         * @param file the file to follow.
    105         * @param listener the TailerListener to use.
    106         * @param delay the delay between checks of the file for new content in milliseconds.
    107         * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    108         */
    109        public Tailer(File file, TailerListener listener, long delay, boolean end) {
    110
    111            this.file = file;
    112            this.delay = delay;
    113            this.end = end;
    114
    115            // Save and prepare the listener
    116            this.listener = listener;
    117            listener.init(this);
    118        }
    119
    120        /**
    121         * Creates and starts a Tailer for the given file.
    122         *
    123         * @param file the file to follow.
    124         * @param listener the TailerListener to use.
    125         * @param delay the delay between checks of the file for new content in milliseconds.
    126         * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    127         * @return The new tailer
    128         */
    129        public static Tailer create(File file, TailerListener listener, long delay, boolean end) {
    130            Tailer tailer = new Tailer(file, listener, delay, end);
    131            Thread thread = new Thread(tailer);
    132            thread.setDaemon(true);
    133            thread.start();
    134            return tailer;
    135        }
    136
    137        /**
    138         * Creates and starts a Tailer for the given file, starting at the beginning of the file
    139         *
    140         * @param file the file to follow.
    141         * @param listener the TailerListener to use.
    142         * @param delay the delay between checks of the file for new content in milliseconds.
    143         * @return The new tailer
    144         */
    145        public static Tailer create(File file, TailerListener listener, long delay) {
    146            return create(file, listener, delay, false);
    147        }
    148
    149        /**
    150         * Creates and starts a Tailer for the given file, starting at the beginning of the file
    151         * with the default delay of 1.0s
    152         *
    153         * @param file the file to follow.
    154         * @param listener the TailerListener to use.
    155         * @return The new tailer
    156         */
    157        public static Tailer create(File file, TailerListener listener) {
    158            return create(file, listener, 1000, false);
    159        }
    160
    161        /**
    162         * Return the file.
    163         *
    164         * @return the file
    165         */
    166        public File getFile() {
    167            return file;
    168        }
    169
    170        /**
    171         * Return the delay.
    172         *
    173         * @return the delay
    174         */
    175        public long getDelay() {
    176            return delay;
    177        }
    178
    179        /**
    180         * Follows changes in the file, calling the TailerListener's handle method for each new line.
    181         */
    182        public void run() {
    183            RandomAccessFile reader = null;
    184            try {
    185                long last = 0; // The last time the file was checked for changes
    186                long position = 0; // position within the file
    187                // Open the file
    188                while (run && reader == null) {
    189                    try {
    190                        reader = new RandomAccessFile(file, "r");
    191                    } catch (FileNotFoundException e) {
    192                        listener.fileNotFound();
    193                    }
    194
    195                    if (reader == null) {
    196                        try {
    197                            Thread.sleep(delay);
    198                        } catch (InterruptedException e) {
    199                        }
    200                    } else {
    201                        // The current position in the file
    202                        position = end ? file.length() : 0;
    203                        last = System.currentTimeMillis();
    204                        reader.seek(position);
    205                    }
    206                }
    207
    208
    209                while (run) {
    210
    211                    // Check the file length to see if it was rotated
    212                    long length = file.length();
    213
    214                    if (length < position) {
    215
    216                        // File was rotated
    217                        listener.fileRotated();
    218
    219                        // Reopen the reader after rotation
    220                        try {
    221                            // Ensure that the old file is closed iff we re-open it successfully
    222                            RandomAccessFile save = reader;
    223                            reader = new RandomAccessFile(file, "r");
    224                            position = 0;
    225                            // close old file explicitly rather than relying on GC picking up previous RAF
    226                            IOUtils.closeQuietly(save);
    227                        } catch (FileNotFoundException e) {
    228                            // in this case we continue to use the previous reader and position values
    229                            listener.fileNotFound();
    230                        }
    231                        continue;
    232                    } else {
    233
    234                        // File was not rotated
    235
    236                        // See if the file needs to be read again
    237                        if (length > position) {
    238
    239                            // The file has more content than it did last time
    240                            last = System.currentTimeMillis();
    241                            position = readLines(reader);
    242
    243                        } else if (FileUtils.isFileNewer(file, last)) {
    244
    245                            /* This can happen if the file is truncated or overwritten
    246                             * with the exact same length of information. In cases like
    247                             * this, the file position needs to be reset
    248                             */
    249                            position = 0;
    250                            reader.seek(position); // cannot be null here
    251
    252                            // Now we can read new lines
    253                            last = System.currentTimeMillis();
    254                            position = readLines(reader);
    255                        }
    256                    }
    257                    try {
    258                        Thread.sleep(delay);
    259                    } catch (InterruptedException e) {
    260                    }
    261                }
    262
    263            } catch (Exception e) {
    264
    265                listener.handle(e);
    266
    267            } finally {
    268                IOUtils.closeQuietly(reader);
    269            }
    270        }
    271
    272        /**
    273         * Allows the tailer to complete its current loop and return.
    274         */
    275        public void stop() {
    276            this.run = false;
    277        }
    278
    279        /**
    280         * Read new lines.
    281         *
    282         * @param reader The file to read
    283         * @return The new position after the lines have been read
    284         * @throws java.io.IOException if an I/O error occurs.
    285         */
    286        private long readLines(RandomAccessFile reader) throws IOException {
    287            String line = reader.readLine();
    288            while (line != null) {
    289                listener.handle(line);
    290                line = reader.readLine();
    291            }
    292            return reader.getFilePointer();
    293        }
    294
    295    }