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 }