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.CR;
20  import static org.apache.commons.io.IOUtils.EOF;
21  import static org.apache.commons.io.IOUtils.LF;
22  
23  import java.io.ByteArrayOutputStream;
24  import java.io.File;
25  import java.io.FileNotFoundException;
26  import java.io.IOException;
27  import java.io.RandomAccessFile;
28  import java.nio.charset.Charset;
29  
30  import org.apache.commons.io.FileUtils;
31  import org.apache.commons.io.IOUtils;
32  
33  /**
34   * Simple implementation of the unix "tail -f" functionality.
35   *
36   * <h2>1. Create a TailerListener implementation</h2>
37   * <p>
38   * First you need to create a {@link TailerListener} implementation
39   * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to
40   * implement every method).
41   * </p>
42   *
43   * <p>For example:</p>
44   * <pre>
45   *  public class MyTailerListener extends TailerListenerAdapter {
46   *      public void handle(String line) {
47   *          System.out.println(line);
48   *      }
49   *  }</pre>
50   *
51   * <h2>2. Using a Tailer</h2>
52   *
53   * <p>
54   * You can create and use a Tailer in one of three ways:
55   * </p>
56   * <ul>
57   *   <li>Using one of the static helper methods:
58   *     <ul>
59   *       <li>{@link Tailer#create(File, TailerListener)}</li>
60   *       <li>{@link Tailer#create(File, TailerListener, long)}</li>
61   *       <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li>
62   *     </ul>
63   *   </li>
64   *   <li>Using an {@link java.util.concurrent.Executor}</li>
65   *   <li>Using an {@link Thread}</li>
66   * </ul>
67   *
68   * <p>
69   * An example of each of these is shown below.
70   * </p>
71   *
72   * <h3>2.1 Using the static helper method</h3>
73   *
74   * <pre>
75   *      TailerListener listener = new MyTailerListener();
76   *      Tailer tailer = Tailer.create(file, listener, delay);</pre>
77   *
78   * <h3>2.2 Using an Executor</h3>
79   *
80   * <pre>
81   *      TailerListener listener = new MyTailerListener();
82   *      Tailer tailer = new Tailer(file, listener, delay);
83   *
84   *      // stupid executor impl. for demo purposes
85   *      Executor executor = new Executor() {
86   *          public void execute(Runnable command) {
87   *              command.run();
88   *           }
89   *      };
90   *
91   *      executor.execute(tailer);
92   * </pre>
93   *
94   *
95   * <h3>2.3 Using a Thread</h3>
96   * <pre>
97   *      TailerListener listener = new MyTailerListener();
98   *      Tailer tailer = new Tailer(file, listener, delay);
99   *      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  */
126 public 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 Tailerut/Tailer.html#Tailer">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 }