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 java.io.File;
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.io.RandomAccessFile;
023
024import org.apache.commons.io.FileUtils;
025import 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 1348698 2012-06-11 01:09:58Z ggregory $
105 * @since 2.0
106 */
107public class Tailer implements Runnable {
108
109    private static final int DEFAULT_DELAY_MILLIS = 1000;
110
111    private static final String RAF_MODE = "r";
112
113    private static final int DEFAULT_BUFSIZE = 4096;
114  
115    /**
116     * Buffer on top of RandomAccessFile.
117     */
118    private final byte inbuf[];
119    
120    /**
121     * The file which will be tailed.
122     */
123    private final File file;
124
125    /**
126     * The amount of time to wait for the file to be updated.
127     */
128    private final long delayMillis;
129
130    /**
131     * Whether to tail from the end or start of file
132     */
133    private final boolean end;
134
135    /**
136     * The listener to notify of events when tailing.
137     */
138    private final TailerListener listener;
139
140    /**
141     * Whether to close and reopen the file whilst waiting for more input.
142     */
143    private final boolean reOpen;
144    
145    /**
146     * The tailer will run as long as this value is true.
147     */
148    private volatile boolean run = true;
149
150    /**
151     * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
152     * @param file The file to follow.
153     * @param listener the TailerListener to use.
154     */
155    public Tailer(File file, TailerListener listener) {
156        this(file, listener, DEFAULT_DELAY_MILLIS);
157    }
158
159    /**
160     * Creates a Tailer for the given file, starting from the beginning.
161     * @param file the file to follow.
162     * @param listener the TailerListener to use.
163     * @param delayMillis the delay between checks of the file for new content in milliseconds.
164     */
165    public Tailer(File file, TailerListener listener, long delayMillis) {
166        this(file, listener, delayMillis, false);
167    }
168
169    /**
170     * Creates a Tailer for the given file, with a delay other than the default 1.0s.
171     * @param file the file to follow.
172     * @param listener the TailerListener to use.
173     * @param delayMillis the delay between checks of the file for new content in milliseconds.
174     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
175     */
176    public Tailer(File file, TailerListener listener, long delayMillis, boolean end) {
177        this(file, listener, delayMillis, end, DEFAULT_BUFSIZE);
178    }
179    
180    /**
181     * Creates a Tailer for the given file, with a delay other than the default 1.0s.
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     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
186     * @param reOpen if true, close and reopen the file between reading chunks 
187     */
188    public Tailer(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen) {
189        this(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE);
190    }
191    
192    /**
193     * Creates a Tailer for the given file, with a specified buffer size.
194     * @param file the file to follow.
195     * @param listener the TailerListener to use.
196     * @param delayMillis the delay between checks of the file for new content in milliseconds.
197     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
198     * @param bufSize Buffer size
199     */
200    public Tailer(File file, TailerListener listener, long delayMillis, boolean end, int bufSize) {
201        this(file, listener, delayMillis, end, false, bufSize);
202    }
203    
204    /**
205     * Creates a Tailer for the given file, with a specified buffer size.
206     * @param file the file to follow.
207     * @param listener the TailerListener to use.
208     * @param delayMillis the delay between checks of the file for new content in milliseconds.
209     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
210     * @param reOpen if true, close and reopen the file between reading chunks 
211     * @param bufSize Buffer size
212     */
213    public Tailer(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen, int bufSize) {
214        this.file = file;
215        this.delayMillis = delayMillis;
216        this.end = end;
217        
218        this.inbuf = new byte[bufSize];
219
220        // Save and prepare the listener
221        this.listener = listener;
222        listener.init(this);
223        this.reOpen = reOpen;
224    }
225    
226    /**
227     * Creates and starts a Tailer for the given file.
228     * 
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 bufSize buffer size.
234     * @return The new tailer
235     */
236    public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end, int bufSize) {
237        Tailer tailer = new Tailer(file, listener, delayMillis, end, bufSize);
238        Thread thread = new Thread(tailer);
239        thread.setDaemon(true);
240        thread.start();
241        return tailer;
242    }
243
244    /**
245     * Creates and starts a Tailer for the given file.
246     * 
247     * @param file the file to follow.
248     * @param listener the TailerListener to use.
249     * @param delayMillis the delay between checks of the file for new content in milliseconds.
250     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
251     * @param reOpen whether to close/reopen the file between chunks
252     * @param bufSize buffer size.
253     * @return The new tailer
254     */
255    public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen, 
256            int bufSize) {
257        Tailer tailer = new Tailer(file, listener, delayMillis, end, reOpen, bufSize);
258        Thread thread = new Thread(tailer);
259        thread.setDaemon(true);
260        thread.start();
261        return tailer;
262    }
263
264    /**
265     * Creates and starts a Tailer for the given file with default buffer size.
266     * 
267     * @param file the file to follow.
268     * @param listener the TailerListener to use.
269     * @param delayMillis the delay between checks of the file for new content in milliseconds.
270     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
271     * @return The new tailer
272     */
273    public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end) {
274        return create(file, listener, delayMillis, end, DEFAULT_BUFSIZE);
275    }
276
277    /**
278     * Creates and starts a Tailer for the given file with default buffer size.
279     * 
280     * @param file the file to follow.
281     * @param listener the TailerListener to use.
282     * @param delayMillis the delay between checks of the file for new content in milliseconds.
283     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
284     * @param reOpen whether to close/reopen the file between chunks
285     * @return The new tailer
286     */
287    public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen) {
288        return create(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE);
289    }
290
291    /**
292     * Creates and starts a Tailer for the given file, starting at the beginning of the file
293     * 
294     * @param file the file to follow.
295     * @param listener the TailerListener to use.
296     * @param delayMillis the delay between checks of the file for new content in milliseconds.
297     * @return The new tailer
298     */
299    public static Tailer create(File file, TailerListener listener, long delayMillis) {
300        return create(file, listener, delayMillis, false);
301    }
302
303    /**
304     * Creates and starts a Tailer for the given file, starting at the beginning of the file
305     * with the default delay of 1.0s
306     * 
307     * @param file the file to follow.
308     * @param listener the TailerListener to use.
309     * @return The new tailer
310     */
311    public static Tailer create(File file, TailerListener listener) {
312        return create(file, listener, DEFAULT_DELAY_MILLIS, false);
313    }
314
315    /**
316     * Return the file.
317     *
318     * @return the file
319     */
320    public File getFile() {
321        return file;
322    }
323
324    /**
325     * Return the delay in milliseconds.
326     *
327     * @return the delay in milliseconds.
328     */
329    public long getDelay() {
330        return delayMillis;
331    }
332
333    /**
334     * Follows changes in the file, calling the TailerListener's handle method for each new line.
335     */
336    public void run() {
337        RandomAccessFile reader = null;
338        try {
339            long last = 0; // The last time the file was checked for changes
340            long position = 0; // position within the file
341            // Open the file
342            while (run && reader == null) {
343                try {
344                    reader = new RandomAccessFile(file, RAF_MODE);
345                } catch (FileNotFoundException e) {
346                    listener.fileNotFound();
347                }
348
349                if (reader == null) {
350                    try {
351                        Thread.sleep(delayMillis);
352                    } catch (InterruptedException e) {
353                    }
354                } else {
355                    // The current position in the file
356                    position = end ? file.length() : 0;
357                    last = System.currentTimeMillis();
358                    reader.seek(position);
359                }
360            }
361
362            while (run) {
363
364                boolean newer = FileUtils.isFileNewer(file, last); // IO-279, must be done first
365
366                // Check the file length to see if it was rotated
367                long length = file.length();
368
369                if (length < position) {
370
371                    // File was rotated
372                    listener.fileRotated();
373
374                    // Reopen the reader after rotation
375                    try {
376                        // Ensure that the old file is closed iff we re-open it successfully
377                        RandomAccessFile save = reader;
378                        reader = new RandomAccessFile(file, RAF_MODE);
379                        position = 0;
380                        // close old file explicitly rather than relying on GC picking up previous RAF
381                        IOUtils.closeQuietly(save);
382                    } catch (FileNotFoundException e) {
383                        // in this case we continue to use the previous reader and position values
384                        listener.fileNotFound();
385                    }
386                    continue;
387                } else {
388
389                    // File was not rotated
390
391                    // See if the file needs to be read again
392                    if (length > position) {
393
394                        // The file has more content than it did last time
395                        position = readLines(reader);
396                        last = System.currentTimeMillis();
397
398                    } else if (newer) {
399
400                        /*
401                         * This can happen if the file is truncated or overwritten with the exact same length of
402                         * information. In cases like this, the file position needs to be reset
403                         */
404                        position = 0;
405                        reader.seek(position); // cannot be null here
406
407                        // Now we can read new lines
408                        position = readLines(reader);
409                        last = System.currentTimeMillis();
410                    }
411                }
412                if (reOpen) {
413                    IOUtils.closeQuietly(reader);
414                }
415                try {
416                    Thread.sleep(delayMillis);
417                } catch (InterruptedException e) {
418                }
419                if (run && reOpen) {
420                    reader = new RandomAccessFile(file, RAF_MODE);
421                    reader.seek(position);
422                }
423            }
424
425        } catch (Exception e) {
426
427            listener.handle(e);
428
429        } finally {
430            IOUtils.closeQuietly(reader);
431        }
432    }
433
434    /**
435     * Allows the tailer to complete its current loop and return.
436     */
437    public void stop() {
438        this.run = false;
439    }
440
441    /**
442     * Read new lines.
443     *
444     * @param reader The file to read
445     * @return The new position after the lines have been read
446     * @throws java.io.IOException if an I/O error occurs.
447     */
448    private long readLines(RandomAccessFile reader) throws IOException {
449        StringBuilder sb = new StringBuilder();
450
451        long pos = reader.getFilePointer();
452        long rePos = pos; // position to re-read
453
454        int num;
455        boolean seenCR = false;
456        while (run && ((num = reader.read(inbuf)) != -1)) {
457            for (int i = 0; i < num; i++) {
458                byte ch = inbuf[i];
459                switch (ch) {
460                case '\n':
461                    seenCR = false; // swallow CR before LF
462                    listener.handle(sb.toString());
463                    sb.setLength(0);
464                    rePos = pos + i + 1;
465                    break;
466                case '\r':
467                    if (seenCR) {
468                        sb.append('\r');
469                    }
470                    seenCR = true;
471                    break;
472                default:
473                    if (seenCR) {
474                        seenCR = false; // swallow final CR
475                        listener.handle(sb.toString());
476                        sb.setLength(0);
477                        rePos = pos + i + 1;
478                    }
479                    sb.append((char) ch); // add character, not its ascii value
480                }
481            }
482
483            pos = reader.getFilePointer();
484        }
485
486        reader.seek(rePos); // Ensure we can re-read if necessary
487        return rePos;
488    }
489
490}