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}