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 }