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     * <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     */
107    public 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    }