View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.io.input;
18  
19  import static org.apache.commons.io.IOUtils.EOF;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.File;
23  import java.io.FileNotFoundException;
24  import java.io.IOException;
25  import java.io.RandomAccessFile;
26  import java.nio.charset.Charset;
27  
28  import org.apache.commons.io.FileUtils;
29  
30  /**
31   * Simple implementation of the unix "tail -f" functionality.
32   * 
33   * <h2>1. Create a TailerListener implementation</h2>
34   * <p>
35   * First you need to create a {@link TailerListener} implementation
36   * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to
37   * implement every method).
38   * </p>
39   *
40   * <p>For example:</p>
41   * <pre>
42   *  public class MyTailerListener extends TailerListenerAdapter {
43   *      public void handle(String line) {
44   *          System.out.println(line);
45   *      }
46   *  }</pre>
47   *
48   * <h2>2. Using a Tailer</h2>
49   *
50   * <p>
51   * You can create and use a Tailer in one of three ways:
52   * </p>
53   * <ul>
54   *   <li>Using one of the static helper methods:
55   *     <ul>
56   *       <li>{@link Tailer#create(File, TailerListener)}</li>
57   *       <li>{@link Tailer#create(File, TailerListener, long)}</li>
58   *       <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li>
59   *     </ul>
60   *   </li>
61   *   <li>Using an {@link java.util.concurrent.Executor}</li>
62   *   <li>Using an {@link Thread}</li>
63   * </ul>
64   *
65   * <p>
66   * An example of each of these is shown below.
67   * </p>
68   *
69   * <h3>2.1 Using the static helper method</h3>
70   *
71   * <pre>
72   *      TailerListener listener = new MyTailerListener();
73   *      Tailer tailer = Tailer.create(file, listener, delay);</pre>
74   *
75   * <h3>2.2 Using an Executor</h3>
76   *
77   * <pre>
78   *      TailerListener listener = new MyTailerListener();
79   *      Tailer tailer = new Tailer(file, listener, delay);
80   *
81   *      // stupid executor impl. for demo purposes
82   *      Executor executor = new Executor() {
83   *          public void execute(Runnable command) {
84   *              command.run();
85   *           }
86   *      };
87   *
88   *      executor.execute(tailer);
89   * </pre>
90   *
91   *
92   * <h3>2.3 Using a Thread</h3>
93   * <pre>
94   *      TailerListener listener = new MyTailerListener();
95   *      Tailer tailer = new Tailer(file, listener, delay);
96   *      Thread thread = new Thread(tailer);
97   *      thread.setDaemon(true); // optional
98   *      thread.start();</pre>
99   *
100  * <h2>3. Stopping a Tailer</h2>
101  * <p>Remember to stop the tailer when you have done with it:</p>
102  * <pre>
103  *      tailer.stop();
104  * </pre>
105  *
106  * <h2>4. Interrupting a Tailer</h2>
107  * <p>You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}.</p>
108  * <pre>
109  *      thread.interrupt();
110  * </pre>
111  * <p>If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}.</p>
112  *
113  * <p>The file is read using the default charset; this can be overridden if necessary</p>
114  * @see TailerListener
115  * @see TailerListenerAdapter
116  * @version $Id$
117  * @since 2.0
118  * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()}
119  */
120 public class Tailer implements Runnable {
121 
122     private static final int DEFAULT_DELAY_MILLIS = 1000;
123 
124     private static final String RAF_MODE = "r";
125 
126     private static final int DEFAULT_BUFSIZE = 4096;
127 
128     // The default charset used for reading files
129     private static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
130 
131     /**
132      * Buffer on top of RandomAccessFile.
133      */
134     private final byte inbuf[];
135 
136     /**
137      * The file which will be tailed.
138      */
139     private final File file;
140 
141     /**
142      * The character set that will be used to read the file.
143      */
144     private final Charset cset;
145 
146     /**
147      * The amount of time to wait for the file to be updated.
148      */
149     private final long delayMillis;
150 
151     /**
152      * Whether to tail from the end or start of file
153      */
154     private final boolean end;
155 
156     /**
157      * The listener to notify of events when tailing.
158      */
159     private final TailerListener listener;
160 
161     /**
162      * Whether to close and reopen the file whilst waiting for more input.
163      */
164     private final boolean reOpen;
165 
166     /**
167      * The tailer will run as long as this value is true.
168      */
169     private volatile boolean run = true;
170 
171     /**
172      * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
173      * @param file The file to follow.
174      * @param listener the TailerListener to use.
175      */
176     public Tailer(final File file, final TailerListener listener) {
177         this(file, listener, DEFAULT_DELAY_MILLIS);
178     }
179 
180     /**
181      * Creates a Tailer for the given file, starting from the beginning.
182      * @param file the file to follow.
183      * @param listener the TailerListener to use.
184      * @param delayMillis the delay between checks of the file for new content in milliseconds.
185      */
186     public Tailer(final File file, final TailerListener listener, final long delayMillis) {
187         this(file, listener, delayMillis, false);
188     }
189 
190     /**
191      * Creates a Tailer for the given file, with a delay other than the default 1.0s.
192      * @param file the file to follow.
193      * @param listener the TailerListener to use.
194      * @param delayMillis the delay between checks of the file for new content in milliseconds.
195      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
196      */
197     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
198         this(file, listener, delayMillis, end, DEFAULT_BUFSIZE);
199     }
200 
201     /**
202      * Creates a Tailer for the given file, with a delay other than the default 1.0s.
203      * @param file the file to follow.
204      * @param listener the TailerListener to use.
205      * @param delayMillis the delay between checks of the file for new content in milliseconds.
206      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
207      * @param reOpen if true, close and reopen the file between reading chunks
208      */
209     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end,
210                   final boolean reOpen) {
211         this(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE);
212     }
213 
214     /**
215      * Creates a Tailer for the given file, with a specified buffer size.
216      * @param file the file to follow.
217      * @param listener the TailerListener to use.
218      * @param delayMillis the delay between checks of the file for new content in milliseconds.
219      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
220      * @param bufSize Buffer size
221      */
222     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end,
223                   final int bufSize) {
224         this(file, listener, delayMillis, end, false, bufSize);
225     }
226 
227     /**
228      * Creates a Tailer for the given file, with a specified buffer size.
229      * @param file the file to follow.
230      * @param listener the TailerListener to use.
231      * @param delayMillis the delay between checks of the file for new content in milliseconds.
232      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
233      * @param reOpen if true, close and reopen the file between reading chunks
234      * @param bufSize Buffer size
235      */
236     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end,
237                   final boolean reOpen, final int bufSize) {
238         this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize);
239     }
240 
241     /**
242      * Creates a Tailer for the given file, with a specified buffer size.
243      * @param file the file to follow.
244      * @param cset the Charset to be used for reading the file
245      * @param listener the TailerListener to use.
246      * @param delayMillis the delay between checks of the file for new content in milliseconds.
247      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
248      * @param reOpen if true, close and reopen the file between reading chunks
249      * @param bufSize Buffer size
250      */
251     public Tailer(final File file, final Charset cset, final TailerListener listener, final long delayMillis,
252                   final boolean end, final boolean reOpen
253             , final int bufSize) {
254         this.file = file;
255         this.delayMillis = delayMillis;
256         this.end = end;
257 
258         this.inbuf = new byte[bufSize];
259 
260         // Save and prepare the listener
261         this.listener = listener;
262         listener.init(this);
263         this.reOpen = reOpen;
264         this.cset = cset; 
265     }
266 
267     /**
268      * Creates and starts a Tailer for the given file.
269      *
270      * @param file the file to follow.
271      * @param listener the TailerListener to use.
272      * @param delayMillis the delay between checks of the file for new content in milliseconds.
273      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
274      * @param bufSize buffer size.
275      * @return The new tailer
276      */
277     public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
278                                 final boolean end, final int bufSize) {
279         return create(file, listener, delayMillis, end, false, bufSize);
280     }
281 
282     /**
283      * Creates and starts a Tailer for the given file.
284      *
285      * @param file the file to follow.
286      * @param listener the TailerListener to use.
287      * @param delayMillis the delay between checks of the file for new content in milliseconds.
288      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
289      * @param reOpen whether to close/reopen the file between chunks
290      * @param bufSize buffer size.
291      * @return The new tailer
292      */
293     public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
294                                 final boolean end, final boolean reOpen,
295             final int bufSize) {
296         return create(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize);
297     }
298 
299     /**
300      * Creates and starts a Tailer for the given file.
301      *
302      * @param file the file to follow.
303      * @param charset the character set to use for reading the file
304      * @param listener the TailerListener to use.
305      * @param delayMillis the delay between checks of the file for new content in milliseconds.
306      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
307      * @param reOpen whether to close/reopen the file between chunks
308      * @param bufSize buffer size.
309      * @return The new tailer
310      */
311     public static Tailer create(final File file, final Charset charset, final TailerListener listener,
312                                 final long delayMillis, final boolean end, final boolean reOpen
313             ,final int bufSize) {
314         final Tailer tailer = new Tailer(file, charset, listener, delayMillis, end, reOpen, bufSize);
315         final Thread thread = new Thread(tailer);
316         thread.setDaemon(true);
317         thread.start();
318         return tailer;
319     }
320 
321     /**
322      * Creates and starts a Tailer for the given file with default buffer size.
323      *
324      * @param file the file to follow.
325      * @param listener the TailerListener to use.
326      * @param delayMillis the delay between checks of the file for new content in milliseconds.
327      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
328      * @return The new tailer
329      */
330     public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
331                                 final boolean end) {
332         return create(file, listener, delayMillis, end, DEFAULT_BUFSIZE);
333     }
334 
335     /**
336      * Creates and starts a Tailer for the given file with default buffer size.
337      *
338      * @param file the file to follow.
339      * @param listener the TailerListener to use.
340      * @param delayMillis the delay between checks of the file for new content in milliseconds.
341      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
342      * @param reOpen whether to close/reopen the file between chunks
343      * @return The new tailer
344      */
345     public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
346                                 final boolean end, final boolean reOpen) {
347         return create(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE);
348     }
349 
350     /**
351      * Creates and starts a Tailer for the given file, starting at the beginning of the file
352      *
353      * @param file the file to follow.
354      * @param listener the TailerListener to use.
355      * @param delayMillis the delay between checks of the file for new content in milliseconds.
356      * @return The new tailer
357      */
358     public static Tailer create(final File file, final TailerListener listener, final long delayMillis) {
359         return create(file, listener, delayMillis, false);
360     }
361 
362     /**
363      * Creates and starts a Tailer for the given file, starting at the beginning of the file
364      * with the default delay of 1.0s
365      *
366      * @param file the file to follow.
367      * @param listener the TailerListener to use.
368      * @return The new tailer
369      */
370     public static Tailer create(final File file, final TailerListener listener) {
371         return create(file, listener, DEFAULT_DELAY_MILLIS, false);
372     }
373 
374     /**
375      * Return the file.
376      *
377      * @return the file
378      */
379     public File getFile() {
380         return file;
381     }
382 
383     /**
384      * Gets whether to keep on running.
385      *
386      * @return whether to keep on running.
387      * @since 2.5
388      */
389     protected boolean getRun() {
390         return run;
391     }
392 
393     /**
394      * Return the delay in milliseconds.
395      *
396      * @return the delay in milliseconds.
397      */
398     public long getDelay() {
399         return delayMillis;
400     }
401 
402     /**
403      * Follows changes in the file, calling the TailerListener's handle method for each new line.
404      */
405     @Override
406     public void run() {
407         RandomAccessFile reader = null;
408         try {
409             long last = 0; // The last time the file was checked for changes
410             long position = 0; // position within the file
411             // Open the file
412             while (getRun() && reader == null) {
413                 try {
414                     reader = new RandomAccessFile(file, RAF_MODE);
415                 } catch (final FileNotFoundException e) {
416                     listener.fileNotFound();
417                 }
418                 if (reader == null) {
419                     Thread.sleep(delayMillis);
420                 } else {
421                     // The current position in the file
422                     position = end ? file.length() : 0;
423                     last = file.lastModified();
424                     reader.seek(position);
425                 }
426             }
427             while (getRun()) {
428                 final boolean newer = FileUtils.isFileNewer(file, last); // IO-279, must be done first
429                 // Check the file length to see if it was rotated
430                 final long length = file.length();
431                 if (length < position) {
432                     // File was rotated
433                     listener.fileRotated();
434                     // Reopen the reader after rotation ensuring that the old file is closed iff we re-open it
435                     // successfully
436                     try (RandomAccessFile save = reader) {
437                         reader = new RandomAccessFile(file, RAF_MODE);
438                         // At this point, we're sure that the old file is rotated
439                         // Finish scanning the old file and then we'll start with the new one
440                         try {
441                             readLines(save);
442                         }  catch (IOException ioe) {
443                             listener.handle(ioe);
444                         }
445                         position = 0;
446                     } catch (final FileNotFoundException e) {
447                         // in this case we continue to use the previous reader and position values
448                         listener.fileNotFound();
449                         Thread.sleep(delayMillis);
450                     }
451                     continue;
452                 } else {
453                     // File was not rotated
454                     // See if the file needs to be read again
455                     if (length > position) {
456                         // The file has more content than it did last time
457                         position = readLines(reader);
458                         last = file.lastModified();
459                     } else if (newer) {
460                         /*
461                          * This can happen if the file is truncated or overwritten with the exact same length of
462                          * information. In cases like this, the file position needs to be reset
463                          */
464                         position = 0;
465                         reader.seek(position); // cannot be null here
466 
467                         // Now we can read new lines
468                         position = readLines(reader);
469                         last = file.lastModified();
470                     }
471                 }
472                 if (reOpen && reader != null) {
473                     reader.close();
474                 }
475                 Thread.sleep(delayMillis);
476                 if (getRun() && reOpen) {
477                     reader = new RandomAccessFile(file, RAF_MODE);
478                     reader.seek(position);
479                 }
480             }
481         } catch (final InterruptedException e) {
482             Thread.currentThread().interrupt();
483             listener.handle(e);
484         } catch (final Exception e) {
485             listener.handle(e);
486         } finally {
487             try {
488                 if (reader != null) {
489                     reader.close();
490                 }
491             }
492             catch (final IOException e) {
493                 listener.handle(e);
494             }
495             stop();
496         }
497     }
498 
499     /**
500      * Allows the tailer to complete its current loop and return.
501      */
502     public void stop() {
503         this.run = false;
504     }
505 
506     /**
507      * Read new lines.
508      *
509      * @param reader The file to read
510      * @return The new position after the lines have been read
511      * @throws java.io.IOException if an I/O error occurs.
512      */
513     private long readLines(final RandomAccessFile reader) throws IOException {
514         try (ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64)) {
515             long pos = reader.getFilePointer();
516             long rePos = pos; // position to re-read
517             int num;
518             boolean seenCR = false;
519             while (getRun() && ((num = reader.read(inbuf)) != EOF)) {
520                 for (int i = 0; i < num; i++) {
521                     final byte ch = inbuf[i];
522                     switch ( ch ) {
523                         case '\n':
524                             seenCR = false; // swallow CR before LF
525                             listener.handle(new String(lineBuf.toByteArray(), cset));
526                             lineBuf.reset();
527                             rePos = pos + i + 1;
528                             break;
529                         case '\r':
530                             if (seenCR) {
531                                 lineBuf.write('\r');
532                             }
533                             seenCR = true;
534                             break;
535                         default:
536                             if (seenCR) {
537                                 seenCR = false; // swallow final CR
538                                 listener.handle(new String(lineBuf.toByteArray(), cset));
539                                 lineBuf.reset();
540                                 rePos = pos + i + 1;
541                             }
542                             lineBuf.write(ch);
543                     }
544                 }
545                 pos = reader.getFilePointer();
546             }
547 
548             reader.seek(rePos); // Ensure we can re-read if necessary
549 
550             if (listener instanceof TailerListenerAdapter) {
551                 ((TailerListenerAdapter) listener).endOfFileReached();
552             }
553 
554             return rePos;
555         }
556     }
557 }