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 1304052 2012-03-22 20:55:29Z ggregory $
105     * @since 2.0
106     */
107    public class Tailer implements Runnable {
108    
109        /**
110         * The file which will be tailed.
111         */
112        private final File file;
113    
114        /**
115         * The amount of time to wait for the file to be updated.
116         */
117        private final long delay;
118    
119        /**
120         * Whether to tail from the end or start of file
121         */
122        private final boolean end;
123    
124        /**
125         * The listener to notify of events when tailing.
126         */
127        private final TailerListener listener;
128    
129        /**
130         * The tailer will run as long as this value is true.
131         */
132        private volatile boolean run = true;
133    
134        /**
135         * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
136         * @param file The file to follow.
137         * @param listener the TailerListener to use.
138         */
139        public Tailer(File file, TailerListener listener) {
140            this(file, listener, 1000);
141        }
142    
143        /**
144         * Creates a Tailer for the given file, starting from the beginning.
145         * @param file the file to follow.
146         * @param listener the TailerListener to use.
147         * @param delay the delay between checks of the file for new content in milliseconds.
148         */
149        public Tailer(File file, TailerListener listener, long delay) {
150            this(file, listener, delay, false);
151        }
152    
153        /**
154         * Creates a Tailer for the given file, with a delay other than the default 1.0s.
155         * @param file the file to follow.
156         * @param listener the TailerListener to use.
157         * @param delay the delay between checks of the file for new content in milliseconds.
158         * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
159         */
160        public Tailer(File file, TailerListener listener, long delay, boolean end) {
161    
162            this.file = file;
163            this.delay = delay;
164            this.end = end;
165    
166            // Save and prepare the listener
167            this.listener = listener;
168            listener.init(this);
169        }
170    
171        /**
172         * Creates and starts a Tailer for the given file.
173         * 
174         * @param file the file to follow.
175         * @param listener the TailerListener to use.
176         * @param delay the delay between checks of the file for new content in milliseconds.
177         * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
178         * @return The new tailer
179         */
180        public static Tailer create(File file, TailerListener listener, long delay, boolean end) {
181            Tailer tailer = new Tailer(file, listener, delay, end);
182            Thread thread = new Thread(tailer);
183            thread.setDaemon(true);
184            thread.start();
185            return tailer;
186        }
187    
188        /**
189         * Creates and starts a Tailer for the given file, starting at the beginning of the file
190         * 
191         * @param file the file to follow.
192         * @param listener the TailerListener to use.
193         * @param delay the delay between checks of the file for new content in milliseconds.
194         * @return The new tailer
195         */
196        public static Tailer create(File file, TailerListener listener, long delay) {
197            return create(file, listener, delay, false);
198        }
199    
200        /**
201         * Creates and starts a Tailer for the given file, starting at the beginning of the file
202         * with the default delay of 1.0s
203         * 
204         * @param file the file to follow.
205         * @param listener the TailerListener to use.
206         * @return The new tailer
207         */
208        public static Tailer create(File file, TailerListener listener) {
209            return create(file, listener, 1000, false);
210        }
211    
212        /**
213         * Return the file.
214         *
215         * @return the file
216         */
217        public File getFile() {
218            return file;
219        }
220    
221        /**
222         * Return the delay.
223         *
224         * @return the delay
225         */
226        public long getDelay() {
227            return delay;
228        }
229    
230        /**
231         * Follows changes in the file, calling the TailerListener's handle method for each new line.
232         */
233        public void run() {
234            RandomAccessFile reader = null;
235            try {
236                long last = 0; // The last time the file was checked for changes
237                long position = 0; // position within the file
238                // Open the file
239                while (run && reader == null) {
240                    try {
241                        reader = new RandomAccessFile(file, "r");
242                    } catch (FileNotFoundException e) {
243                        listener.fileNotFound();
244                    }
245    
246                    if (reader == null) {
247                        try {
248                            Thread.sleep(delay);
249                        } catch (InterruptedException e) {
250                        }
251                    } else {
252                        // The current position in the file
253                        position = end ? file.length() : 0;
254                        last = System.currentTimeMillis();
255                        reader.seek(position);                    
256                    }
257                }
258    
259    
260                while (run) {
261    
262                    // Check the file length to see if it was rotated
263                    long length = file.length();
264    
265                    if (length < position) {
266    
267                        // File was rotated
268                        listener.fileRotated();
269    
270                        // Reopen the reader after rotation
271                        try {
272                            // Ensure that the old file is closed iff we re-open it successfully
273                            RandomAccessFile save = reader;
274                            reader = new RandomAccessFile(file, "r");
275                            position = 0;
276                            // close old file explicitly rather than relying on GC picking up previous RAF
277                            IOUtils.closeQuietly(save);
278                        } catch (FileNotFoundException e) {
279                            // in this case we continue to use the previous reader and position values
280                            listener.fileNotFound();
281                        }
282                        continue;
283                    } else {
284    
285                        // File was not rotated
286    
287                        // See if the file needs to be read again
288                        if (length > position) {
289    
290                            // The file has more content than it did last time
291                            last = System.currentTimeMillis();
292                            position = readLines(reader);
293    
294                        } else if (FileUtils.isFileNewer(file, last)) {
295    
296                            /* This can happen if the file is truncated or overwritten
297                             * with the exact same length of information. In cases like
298                             * this, the file position needs to be reset
299                             */
300                            position = 0;
301                            reader.seek(position); // cannot be null here
302    
303                            // Now we can read new lines
304                            last = System.currentTimeMillis();
305                            position = readLines(reader);
306                        }
307                    }
308                    try {
309                        Thread.sleep(delay);
310                    } catch (InterruptedException e) {
311                    }
312                }
313    
314            } catch (Exception e) {
315    
316                listener.handle(e);
317    
318            } finally {
319                IOUtils.closeQuietly(reader);
320            }
321        }
322    
323        /**
324         * Allows the tailer to complete its current loop and return.
325         */
326        public void stop() {
327            this.run = false;
328        }
329    
330        /**
331         * Read new lines.
332         *
333         * @param reader The file to read
334         * @return The new position after the lines have been read
335         * @throws java.io.IOException if an I/O error occurs.
336         */
337        private long readLines(RandomAccessFile reader) throws IOException {
338            long pos = reader.getFilePointer();
339            String line = readLine(reader);
340            while (line != null) {
341                pos = reader.getFilePointer();
342                listener.handle(line);
343                line = readLine(reader);
344            }
345            reader.seek(pos); // Ensure we can re-read if necessary
346            return pos;
347        }
348    
349        /**
350         * Version of readline() that returns null on EOF rather than a partial line.
351         * @param reader the input file
352         * @return the line, or null if EOF reached before '\n' is seen.
353         * @throws IOException if an error occurs.
354         */
355        private String readLine(RandomAccessFile reader) throws IOException {
356            StringBuffer sb  = new StringBuffer();
357            int ch;
358            boolean seenCR = false;
359            while((ch=reader.read()) != -1) {
360                switch(ch) {
361                    case '\n':
362                        return sb.toString();
363                    case '\r':
364                        seenCR = true;
365                        break;
366                    default:
367                        if (seenCR) {
368                            sb.append('\r');
369                            seenCR = false;
370                        }
371                        sb.append((char)ch); // add character, not its ascii value
372                }
373            }
374            return null;
375        }
376    }