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 }