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 }