1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more3 * contributor license agreements. See the NOTICE file distributed with4 * this work for additional information regarding copyright ownership.5 * The ASF licenses this file to You under the Apache License, Version 2.06 * (the "License"); you may not use this file except in compliance with7 * the License. You may obtain a copy of the License at8 *9 *http://www.apache.org/licenses/LICENSE-2.0
10 *11 * Unless required by applicable law or agreed to in writing, software12 * distributed under the License is distributed on an "AS IS" BASIS,13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.14 * See the License for the specific language governing permissions and15 * limitations under the License.16 */
17 packagecom.snow.tailer;18
19 importorg.apache.commons.io.FileUtils;20 importorg.apache.commons.io.IOUtils;21 importorg.slf4j.Logger;22 importorg.slf4j.LoggerFactory;23
24 importjava.io.File;25 importjava.io.FileNotFoundException;26 importjava.io.IOException;27 importjava.io.RandomAccessFile;28
29 /**
30 * Simple implementation of the unix "tail -f" functionality.31 *
32 *
1. Create a TailerListener implementation33 *
34 * First you need to create a {@linkTailerListener} implementation35 * ({@linkTailerListenerAdapter} is provided for convenience so that you don't have to36 * implement every method).37 *
38 *39 *For example:
40 *41 * public class MyTailerListener extends TailerListenerAdapter {42 * public void handle(String line) {43 * System.out.println(line);44 * }45 * }46 *47 *48 *
2. Using a Tailer
49 *50 * You can create and use a Tailer in one of three ways:51 *- 52 *
- Using one of the static helper methods:53 *
- 54 *
- {@linkTailer#create(File, TailerListener)}55 *
- {@linkTailer#create(File, TailerListener, long)}56 *
- {@linkTailer#create(File, TailerListener, long, boolean)}57 *
- Using an {@linkjava.util.concurrent.Executor}60 *
- Using an {@linkThread}61 *
2.1 Using the static helper method
66 *67 *68 * TailerListener listener = new MyTailerListener();69 * Tailer tailer = Tailer.create(file, listener, delay);70 *71 *72 *
2.2 Use an Executor
73 *74 *75 * TailerListener listener = new MyTailerListener();76 * Tailer tailer = new Tailer(file, listener, delay);77 *78 * // stupid executor impl. for demo purposes79 * Executor executor = new Executor() {80 * public void execute(Runnable command) {81 * command.run();82 * }83 * };84 *85 * executor.execute(tailer);86 *87 *88 *89 *
2.3 Use a Thread
90 *91 * TailerListener listener = new MyTailerListener();92 * Tailer tailer = new Tailer(file, listener, delay);93 * Thread thread = new Thread(tailer);94 * thread.setDaemon(true); // optional95 * thread.start();96 *97 *98 *
3. Stop Tailing99 *
Remember to stop the tailer when you have done with it:
100 *101 * tailer.stop();102 *103 *104 *@seeTailerListener105 *@seeTailerListenerAdapter106 *@version$Id: Tailer.java 1348698 2012-06-11 01:09:58Z ggregory $107 *@since2.0108 */
109 public class Tailer implementsRunnable {110 private static final Logger logger = LoggerFactory.getLogger(Tailer.class);111
112 private static final int DEFAULT_DELAY_MILLIS = 1000;113
114 private static final String RAF_MODE = "r";115
116 private static final int DEFAULT_BUFSIZE = 4096;117
118 /**
119 * Buffer on top of RandomAccessFile.120 */
121 private final byteinbuf[];122
123 /**
124 * The file which will be tailed.125 */
126 private finalFile file;127
128 /**
129 * The amount of time to wait for the file to be updated.130 */
131 private final longdelayMillis;132
133 /**
134 * Whether to tail from the end or start of file135 */
136 private final booleanend;137
138 /**
139 * The listener to notify of events when tailing.140 */
141 private finalTailerListener listener;142
143 /**
144 * Whether to close and reopen the file whilst waiting for more input.145 */
146 private final booleanreOpen;147
148 /**
149 * The tailer will run as long as this value is true.150 */
151 private volatile boolean run = true;152 private volatile boolean resetFilePositionIfOverwrittenWithTheSameLength = false;153
154 /**
155 * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.156 *@paramfile The file to follow.157 *@paramlistener the TailerListener to use.158 */
159 publicTailer(File file, TailerListener listener) {160 this(file, listener, DEFAULT_DELAY_MILLIS);161 }162
163 /**
164 * Creates a Tailer for the given file, starting from the beginning.165 *@paramfile the file to follow.166 *@paramlistener the TailerListener to use.167 *@paramdelayMillis the delay between checks of the file for new content in milliseconds.168 */
169 public Tailer(File file, TailerListener listener, longdelayMillis) {170 this(file, listener, delayMillis, false);171 }172
173 /**
174 * Creates a Tailer for the given file, with a delay other than the default 1.0s.175 *@paramfile the file to follow.176 *@paramlistener the TailerListener to use.177 *@paramdelayMillis the delay between checks of the file for new content in milliseconds.178 *@paramend Set to true to tail from the end of the file, false to tail from the beginning of the file.179 */
180 public Tailer(File file, TailerListener listener, long delayMillis, booleanend) {181 this(file, listener, delayMillis, end, DEFAULT_BUFSIZE);182 logger.info("Tailer inited from customer class.");183 }184
185 /**
186 * Creates a Tailer for the given file, with a delay other than the default 1.0s.187 *@paramfile the file to follow.188 *@paramlistener the TailerListener to use.189 *@paramdelayMillis the delay between checks of the file for new content in milliseconds.190 *@paramend Set to true to tail from the end of the file, false to tail from the beginning of the file.191 *@paramreOpen if true, close and reopen the file between reading chunks192 */
193 public Tailer(File file, TailerListener listener, long delayMillis, boolean end, booleanreOpen) {194 this(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE);195 }196
197 /**
198 * Creates a Tailer for the given file, with a specified buffer size.199 *@paramfile the file to follow.200 *@paramlistener the TailerListener to use.201 *@paramdelayMillis the delay between checks of the file for new content in milliseconds.202 *@paramend Set to true to tail from the end of the file, false to tail from the beginning of the file.203 *@parambufSize Buffer size204 */
205 public Tailer(File file, TailerListener listener, long delayMillis, boolean end, intbufSize) {206 this(file, listener, delayMillis, end, false, bufSize);207 }208
209 /**
210 * Creates a Tailer for the given file, with a specified buffer size.211 *@paramfile the file to follow.212 *@paramlistener the TailerListener to use.213 *@paramdelayMillis the delay between checks of the file for new content in milliseconds.214 *@paramend Set to true to tail from the end of the file, false to tail from the beginning of the file.215 *@paramreOpen if true, close and reopen the file between reading chunks216 *@parambufSize Buffer size217 */
218 public Tailer(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen, intbufSize) {219 this.file =file;220 this.delayMillis =delayMillis;221 this.end =end;222
223 this.inbuf = new byte[bufSize];224
225 //Save and prepare the listener
226 this.listener =listener;227 listener.init(this);228 this.reOpen =reOpen;229 }230
231 /**
232 * Creates and starts a Tailer for the given file.233 *234 *@paramfile the file to follow.235 *@paramlistener the TailerListener to use.236 *@paramdelayMillis the delay between checks of the file for new content in milliseconds.237 *@paramend Set to true to tail from the end of the file, false to tail from the beginning of the file.238 *@parambufSize buffer size.239 *@returnThe new tailer240 */
241 public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end, intbufSize) {242 Tailer tailer = newTailer(file, listener, delayMillis, end, bufSize);243 Thread thread = newThread(tailer);244 thread.setDaemon(true);245 thread.start();246 returntailer;247 }248
249 /**
250 * Creates and starts a Tailer for the given file.251 *252 *@paramfile the file to follow.253 *@paramlistener the TailerListener to use.254 *@paramdelayMillis the delay between checks of the file for new content in milliseconds.255 *@paramend Set to true to tail from the end of the file, false to tail from the beginning of the file.256 *@paramreOpen whether to close/reopen the file between chunks257 *@parambufSize buffer size.258 *@returnThe new tailer259 */
260 public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen, intbufSize) {261 Tailer tailer = newTailer(file, listener, delayMillis, end, reOpen, bufSize);262 Thread thread = newThread(tailer);263 thread.setDaemon(true);264 thread.start();265 returntailer;266 }267
268 /**
269 * Creates and starts a Tailer for the given file with default buffer size.270 *271 *@paramfile the file to follow.272 *@paramlistener the TailerListener to use.273 *@paramdelayMillis the delay between checks of the file for new content in milliseconds.274 *@paramend Set to true to tail from the end of the file, false to tail from the beginning of the file.275 *@returnThe new tailer276 */
277 public static Tailer create(File file, TailerListener listener, long delayMillis, booleanend) {278 returncreate(file, listener, delayMillis, end, DEFAULT_BUFSIZE);279 }280
281 /**
282 * Creates and starts a Tailer for the given file with default buffer size.283 *284 *@paramfile the file to follow.285 *@paramlistener the TailerListener to use.286 *@paramdelayMillis the delay between checks of the file for new content in milliseconds.287 *@paramend Set to true to tail from the end of the file, false to tail from the beginning of the file.288 *@paramreOpen whether to close/reopen the file between chunks289 *@returnThe new tailer290 */
291 public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end, booleanreOpen) {292 returncreate(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE);293 }294
295 /**
296 * Creates and starts a Tailer for the given file, starting at the beginning of the file297 *298 *@paramfile the file to follow.299 *@paramlistener the TailerListener to use.300 *@paramdelayMillis the delay between checks of the file for new content in milliseconds.301 *@returnThe new tailer302 */
303 public static Tailer create(File file, TailerListener listener, longdelayMillis) {304 return create(file, listener, delayMillis, false);305 }306
307 /**
308 * Creates and starts a Tailer for the given file, starting at the beginning of the file309 * with the default delay of 1.0s310 *311 *@paramfile the file to follow.312 *@paramlistener the TailerListener to use.313 *@returnThe new tailer314 */
315 public staticTailer create(File file, TailerListener listener) {316 return create(file, listener, DEFAULT_DELAY_MILLIS, false);317 }318
319 /**
320 * Return the file.321 *322 *@returnthe file323 */
324 publicFile getFile() {325 returnfile;326 }327
328 /**
329 * Return the delay in milliseconds.330 *331 *@returnthe delay in milliseconds.332 */
333 public longgetDelay() {334 returndelayMillis;335 }336
337 /**
338 * Follows changes in the file, calling the TailerListener's handle method for each new line.339 */
340 @Override341 public voidrun() {342 RandomAccessFile reader = null;343 try{344 long last = 0; //The last time the file was checked for changes
345 long position = 0; //position within the file346 //Open the file
347 while (run && reader == null) {348 try{349 reader = newRandomAccessFile(file, RAF_MODE);350 } catch(FileNotFoundException e) {351 listener.fileNotFound();352 }353
354 if (reader == null) {355 try{356 Thread.sleep(delayMillis);357 } catch(InterruptedException e) {358 }359 } else{360 //The current position in the file
361 position = end ? file.length() : 0;362 last =file.lastModified();363 reader.seek(position);364 }365 }366
367 while(run) {368
369 boolean newer = FileUtils.isFileNewer(file, last); //IO-279, must be done first370
371 //Check the file length to see if it was rotated
372 long length =file.length();373
374 if (length
377 listener.fileRotated();378
379 //Reopen the reader after rotation
380 try{381 //Ensure that the old file is closed iff we re-open it successfully
382 RandomAccessFile save =reader;383 reader = newRandomAccessFile(file, RAF_MODE);384 position = 0;385 //close old file explicitly rather than relying on GC picking up previous386 //RAF
387 IOUtils.closeQuietly(save);388 } catch(FileNotFoundException e) {389 //in this case we continue to use the previous reader and position values
390 listener.fileNotFound();391 }392 continue;393 } else{394
395 //File was not rotated396
397 //See if the file needs to be read again
398 if (length >position) {399
400 //The file has more content than it did last time
401 position =readLines(reader);402 last =file.lastModified();403
404 } else if(newer) {405 logger.info(String.format("newer, legth=%s, position=%s", length, position));406 if(resetFilePositionIfOverwrittenWithTheSameLength) {407 /*
408 * This can happen if the file is truncated or overwritten with the exact same length of409 * information. In cases like this, the file position needs to be reset410 */
411 position = 0;412 reader.seek(position); //cannot be null here413
414 //Now we can read new lines
415 position =readLines(reader);416 }417 last =file.lastModified();418 }419 }420 if(reOpen) {421 IOUtils.closeQuietly(reader);422 }423 try{424 Thread.sleep(delayMillis);425 } catch(InterruptedException e) {426 }427 if (run &&reOpen) {428 reader = newRandomAccessFile(file, RAF_MODE);429 reader.seek(position);430 logger.info(String.format("reopen, legth=%s, position=%s", length, position));431 }432 }433
434 } catch(Exception e) {435
436 listener.handle(e);437
438 } finally{439 IOUtils.closeQuietly(reader);440 }441 }442
443 /**
444 * Allows the tailer to complete its current loop and return.445 */
446 public voidstop() {447 this.run = false;448 }449
450 /**
451 * Read new lines.452 *453 *@paramreader The file to read454 *@returnThe new position after the lines have been read455 *@throwsIOException if an I/O error occurs.456 */
457 private long readLines(RandomAccessFile reader) throwsIOException {458 StringBuilder sb = newStringBuilder();459
460 long pos =reader.getFilePointer();461 long rePos = pos; //position to re-read
462
463 intnum;464 boolean seenCR = false;465 while (run && ((num = reader.read(inbuf)) != -1)) {466 for (int i = 0; i < num; i++) {467 byte ch =inbuf[i];468 switch(ch) {469 case '\n':470 seenCR = false; //swallow CR before LF
471 listener.handle(sb.toString());472 sb.setLength(0);473 rePos = pos + i + 1;474 break;475 case '\r':476 if(seenCR) {477 sb.append('\r');478 }479 seenCR = true;480 break;481 default:482 if(seenCR) {483 seenCR = false; //swallow final CR
484 listener.handle(sb.toString());485 sb.setLength(0);486 rePos = pos + i + 1;487 }488 sb.append((char) ch); //add character, not its ascii value
489 }490 }491
492 pos =reader.getFilePointer();493 }494
495 reader.seek(rePos); //Ensure we can re-read if necessary
496 returnrePos;497 }498
499 }