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     * Example Usage:
031     * <pre>
032     *      // Simplest invocation using static helper method:
033     *      TailerListener listener = ...
034     *      Tailer tailer = Tailer.create(file, listener, delay);
035     *      
036     *      // Alternative method using executor:
037     *      TailerListener listener = ...
038     *      Tailer tailer = new Tailer(file, listener, delay);
039     *      Executor executor ...
040     *      executor.execute(tailer);
041     *      
042     *      // Alternative method if you want to handle the threading yourself:
043     *      TailerListener listener = ...
044     *      Tailer tailer = new Tailer(file, listener, delay);
045     *      Thread thread = new Thread(tailer);
046     *      thread.setDaemon(true); // optional
047     *      thread.start();
048     *      
049     *      // Remember to stop the tailer when you have done with it:
050     *      tailer.stop();
051     * </pre>
052     *
053     * @version $Id: Tailer.java 1021884 2010-10-12 18:49:16Z ggregory $
054     * @since Commons IO 2.0
055     */
056    public class Tailer implements Runnable {
057    
058        /**
059         * The file which will be tailed.
060         */
061        private final File file;
062    
063        /**
064         * The amount of time to wait for the file to be updated.
065         */
066        private final long delay;
067    
068        /**
069         * Whether to tail from the end or start of file
070         */
071        private final boolean end;
072    
073        /**
074         * The listener to notify of events when tailing.
075         */
076        private final TailerListener listener;
077    
078        /**
079         * The tailer will run as long as this value is true.
080         */
081        private volatile boolean run = true;
082    
083        /**
084         * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
085         * @param file The file to follow.
086         * @param listener the TailerListener to use.
087         */
088        public Tailer(File file, TailerListener listener) {
089            this(file, listener, 1000);
090        }
091    
092        /**
093         * Creates a Tailer for the given file, starting from the beginning.
094         * @param file the file to follow.
095         * @param listener the TailerListener to use.
096         * @param delay the delay between checks of the file for new content in milliseconds.
097         */
098        public Tailer(File file, TailerListener listener, long delay) {
099            this(file, listener, 1000, false);
100        }
101    
102        /**
103         * Creates a Tailer for the given file, with a delay other than the default 1.0s.
104         * @param file the file to follow.
105         * @param listener the TailerListener to use.
106         * @param delay the delay between checks of the file for new content in milliseconds.
107         * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
108         */
109        public Tailer(File file, TailerListener listener, long delay, boolean end) {
110    
111            this.file = file;
112            this.delay = delay;
113            this.end = end;
114    
115            // Save and prepare the listener
116            this.listener = listener;
117            listener.init(this);
118        }
119    
120        /**
121         * Creates and starts a Tailer for the given file.
122         * 
123         * @param file the file to follow.
124         * @param listener the TailerListener to use.
125         * @param delay the delay between checks of the file for new content in milliseconds.
126         * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
127         * @return The new tailer
128         */
129        public static Tailer create(File file, TailerListener listener, long delay, boolean end) {
130            Tailer tailer = new Tailer(file, listener, delay, end);
131            Thread thread = new Thread(tailer);
132            thread.setDaemon(true);
133            thread.start();
134            return tailer;
135        }
136    
137        /**
138         * Creates and starts a Tailer for the given file, starting at the beginning of the file
139         * 
140         * @param file the file to follow.
141         * @param listener the TailerListener to use.
142         * @param delay the delay between checks of the file for new content in milliseconds.
143         * @return The new tailer
144         */
145        public static Tailer create(File file, TailerListener listener, long delay) {
146            return create(file, listener, delay, false);
147        }
148    
149        /**
150         * Creates and starts a Tailer for the given file, starting at the beginning of the file
151         * with the default delay of 1.0s
152         * 
153         * @param file the file to follow.
154         * @param listener the TailerListener to use.
155         * @return The new tailer
156         */
157        public static Tailer create(File file, TailerListener listener) {
158            return create(file, listener, 1000, false);
159        }
160    
161        /**
162         * Return the file.
163         *
164         * @return the file
165         */
166        public File getFile() {
167            return file;
168        }
169    
170        /**
171         * Return the delay.
172         *
173         * @return the delay
174         */
175        public long getDelay() {
176            return delay;
177        }
178    
179        /**
180         * Follows changes in the file, calling the TailerListener's handle method for each new line.
181         */
182        public void run() {
183            RandomAccessFile reader = null;
184            try {
185                long last = 0; // The last time the file was checked for changes
186                long position = 0; // position within the file
187                // Open the file
188                while (run && reader == null) {
189                    try {
190                        reader = new RandomAccessFile(file, "r");
191                    } catch (FileNotFoundException e) {
192                        listener.fileNotFound();
193                    }
194    
195                    if (reader == null) {
196                        try {
197                            Thread.sleep(delay);
198                        } catch (InterruptedException e) {
199                        }
200                    } else {
201                        // The current position in the file
202                        position = end ? file.length() : 0;
203                        last = System.currentTimeMillis();
204                        reader.seek(position);                    
205                    }
206                }
207    
208    
209                while (run) {
210    
211                    // Check the file length to see if it was rotated
212                    long length = file.length();
213    
214                    if (length < position) {
215    
216                        // File was rotated
217                        listener.fileRotated();
218    
219                        // Reopen the reader after rotation
220                        try {
221                            // Ensure that the old file is closed iff we re-open it successfully
222                            RandomAccessFile save = reader;
223                            reader = new RandomAccessFile(file, "r");
224                            position = 0;
225                            // close old file explicitly rather than relying on GC picking up previous RAF
226                            IOUtils.closeQuietly(save);
227                        } catch (FileNotFoundException e) {
228                            // in this case we continue to use the previous reader and position values
229                            listener.fileNotFound();
230                        }
231                        continue;
232                    } else {
233    
234                        // File was not rotated
235    
236                        // See if the file needs to be read again
237                        if (length > position) {
238    
239                            // The file has more content than it did last time
240                            last = System.currentTimeMillis();
241                            position = readLines(reader);
242    
243                        } else if (FileUtils.isFileNewer(file, last)) {
244    
245                            /* This can happen if the file is truncated or overwritten
246                             * with the exact same length of information. In cases like
247                             * this, the file position needs to be reset
248                             */
249                            position = 0;
250                            reader.seek(position); // cannot be null here
251    
252                            // Now we can read new lines
253                            last = System.currentTimeMillis();
254                            position = readLines(reader);
255                        }
256                    }
257                    try {
258                        Thread.sleep(delay);
259                    } catch (InterruptedException e) {
260                    }
261                }
262    
263            } catch (Exception e) {
264    
265                listener.handle(e);
266    
267            } finally {
268                IOUtils.closeQuietly(reader);
269            }
270        }
271    
272        /**
273         * Allows the tailer to complete its current loop and return.
274         */
275        public void stop() {
276            this.run = false;
277        }
278    
279        /**
280         * Read new lines.
281         *
282         * @param reader The file to read
283         * @return The new position after the lines have been read
284         * @throws java.io.IOException if an I/O error occurs.
285         */
286        private long readLines(RandomAccessFile reader) throws IOException {
287            String line = reader.readLine();
288            while (line != null) {
289                listener.handle(line);
290                line = reader.readLine();
291            }
292            return reader.getFilePointer();
293        }
294    
295    }