Coverage Report - org.apache.commons.io.input.Tailer
 
Classes in this File Line Coverage Branch Coverage Complexity
Tailer
86%
103/119
94%
33/35
2.211
 
 1  
 /*
 2  
  * Licensed to the Apache Software Foundation (ASF) under one or more
 3  
  * contributor license agreements.  See the NOTICE file distributed with
 4  
  * this work for additional information regarding copyright ownership.
 5  
  * The ASF licenses this file to You under the Apache License, Version 2.0
 6  
  * (the "License"); you may not use this file except in compliance with
 7  
  * the License.  You may obtain a copy of the License at
 8  
  *
 9  
  *      http://www.apache.org/licenses/LICENSE-2.0
 10  
  *
 11  
  * Unless required by applicable law or agreed to in writing, software
 12  
  * 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 and
 15  
  * limitations under the License.
 16  
  */
 17  
 package org.apache.commons.io.input;
 18  
 
 19  
 import java.io.ByteArrayOutputStream;
 20  
 import java.io.File;
 21  
 import java.io.FileNotFoundException;
 22  
 import java.io.IOException;
 23  
 import java.io.RandomAccessFile;
 24  
 import java.nio.charset.Charset;
 25  
 
 26  
 import org.apache.commons.io.FileUtils;
 27  
 import org.apache.commons.io.IOUtils;
 28  
 
 29  
 /**
 30  
  * Simple implementation of the unix "tail -f" functionality.
 31  
  * <p>
 32  
  * <h2>1. Create a TailerListener implementation</h3>
 33  
  * <p>
 34  
  * First you need to create a {@link TailerListener} implementation
 35  
  * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to
 36  
  * implement every method).
 37  
  * </p>
 38  
  *
 39  
  * <p>For example:</p>
 40  
  * <pre>
 41  
  *  public class MyTailerListener extends TailerListenerAdapter {
 42  
  *      public void handle(String line) {
 43  
  *          System.out.println(line);
 44  
  *      }
 45  
  *  }</pre>
 46  
  *
 47  
  * <h2>2. Using a Tailer</h2>
 48  
  *
 49  
  * You can create and use a Tailer in one of three ways:
 50  
  * <ul>
 51  
  *   <li>Using one of the static helper methods:
 52  
  *     <ul>
 53  
  *       <li>{@link Tailer#create(File, TailerListener)}</li>
 54  
  *       <li>{@link Tailer#create(File, TailerListener, long)}</li>
 55  
  *       <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li>
 56  
  *     </ul>
 57  
  *   </li>
 58  
  *   <li>Using an {@link java.util.concurrent.Executor}</li>
 59  
  *   <li>Using an {@link Thread}</li>
 60  
  * </ul>
 61  
  *
 62  
  * An example of each of these is shown below.
 63  
  *
 64  
  * <h3>2.1 Using the static helper method</h3>
 65  
  *
 66  
  * <pre>
 67  
  *      TailerListener listener = new MyTailerListener();
 68  
  *      Tailer tailer = Tailer.create(file, listener, delay);</pre>
 69  
  *
 70  
  * <h3>2.2 Using an Executor</h3>
 71  
  *
 72  
  * <pre>
 73  
  *      TailerListener listener = new MyTailerListener();
 74  
  *      Tailer tailer = new Tailer(file, listener, delay);
 75  
  *
 76  
  *      // stupid executor impl. for demo purposes
 77  
  *      Executor executor = new Executor() {
 78  
  *          public void execute(Runnable command) {
 79  
  *              command.run();
 80  
  *           }
 81  
  *      };
 82  
  *
 83  
  *      executor.execute(tailer);
 84  
  * </pre>
 85  
  *
 86  
  *
 87  
  * <h3>2.3 Using a Thread</h3>
 88  
  * <pre>
 89  
  *      TailerListener listener = new MyTailerListener();
 90  
  *      Tailer tailer = new Tailer(file, listener, delay);
 91  
  *      Thread thread = new Thread(tailer);
 92  
  *      thread.setDaemon(true); // optional
 93  
  *      thread.start();</pre>
 94  
  *
 95  
  * <h2>3. Stopping a Tailer</h3>
 96  
  * <p>Remember to stop the tailer when you have done with it:</p>
 97  
  * <pre>
 98  
  *      tailer.stop();
 99  
  * </pre>
 100  
  *
 101  
  * <h2>4. Interrupting a Tailer</h3>
 102  
  * <p>You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}.</p>
 103  
  * <pre>
 104  
  *      thread.interrupt();
 105  
  * </pre>
 106  
  * <p>If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}.</p>
 107  
  *
 108  
  * @see TailerListener
 109  
  * @see TailerListenerAdapter
 110  
  * @version $Id: Tailer.java 1476097 2013-04-26 08:26:01Z sebb $
 111  
  * @since 2.0
 112  
  * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()}
 113  
  */
 114  
 public class Tailer implements Runnable {
 115  
 
 116  
     private static final int DEFAULT_DELAY_MILLIS = 1000;
 117  
 
 118  
     private static final String RAF_MODE = "r";
 119  
 
 120  
     private static final int DEFAULT_BUFSIZE = 4096;
 121  
 
 122  
     /**
 123  
      * Buffer on top of RandomAccessFile.
 124  
      */
 125  
     private final byte inbuf[];
 126  
 
 127  
     /**
 128  
      * The file which will be tailed.
 129  
      */
 130  
     private final File file;
 131  
 
 132  
     /**
 133  
      * The amount of time to wait for the file to be updated.
 134  
      */
 135  
     private final long delayMillis;
 136  
 
 137  
     /**
 138  
      * Whether to tail from the end or start of file
 139  
      */
 140  
     private final boolean end;
 141  
 
 142  
     /**
 143  
      * The listener to notify of events when tailing.
 144  
      */
 145  
     private final TailerListener listener;
 146  
 
 147  
     /**
 148  
      * Whether to close and reopen the file whilst waiting for more input.
 149  
      */
 150  
     private final boolean reOpen;
 151  
 
 152  
     /**
 153  
      * The tailer will run as long as this value is true.
 154  
      */
 155  9
     private volatile boolean run = true;
 156  
 
 157  
     /**
 158  
      * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
 159  
      * @param file The file to follow.
 160  
      * @param listener the TailerListener to use.
 161  
      */
 162  
     public Tailer(final File file, final TailerListener listener) {
 163  0
         this(file, listener, DEFAULT_DELAY_MILLIS);
 164  0
     }
 165  
 
 166  
     /**
 167  
      * Creates a Tailer for the given file, starting from the beginning.
 168  
      * @param file the file to follow.
 169  
      * @param listener the TailerListener to use.
 170  
      * @param delayMillis the delay between checks of the file for new content in milliseconds.
 171  
      */
 172  
     public Tailer(final File file, final TailerListener listener, final long delayMillis) {
 173  0
         this(file, listener, delayMillis, false);
 174  0
     }
 175  
 
 176  
     /**
 177  
      * Creates a Tailer for the given file, with a delay other than the default 1.0s.
 178  
      * @param file the file to follow.
 179  
      * @param listener the TailerListener to use.
 180  
      * @param delayMillis the delay between checks of the file for new content in milliseconds.
 181  
      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
 182  
      */
 183  
     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
 184  4
         this(file, listener, delayMillis, end, DEFAULT_BUFSIZE);
 185  4
     }
 186  
 
 187  
     /**
 188  
      * Creates a Tailer for the given file, with a delay other than the default 1.0s.
 189  
      * @param file the file to follow.
 190  
      * @param listener the TailerListener to use.
 191  
      * @param delayMillis the delay between checks of the file for new content in milliseconds.
 192  
      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
 193  
      * @param reOpen if true, close and reopen the file between reading chunks
 194  
      */
 195  
     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) {
 196  2
         this(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE);
 197  2
     }
 198  
 
 199  
     /**
 200  
      * Creates a Tailer for the given file, with a specified buffer size.
 201  
      * @param file the file to follow.
 202  
      * @param listener the TailerListener to use.
 203  
      * @param delayMillis the delay between checks of the file for new content in milliseconds.
 204  
      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
 205  
      * @param bufSize Buffer size
 206  
      */
 207  
     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufSize) {
 208  6
         this(file, listener, delayMillis, end, false, bufSize);
 209  6
     }
 210  
 
 211  
     /**
 212  
      * Creates a Tailer for the given file, with a specified buffer size.
 213  
      * @param file the file to follow.
 214  
      * @param listener the TailerListener to use.
 215  
      * @param delayMillis the delay between checks of the file for new content in milliseconds.
 216  
      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
 217  
      * @param reOpen if true, close and reopen the file between reading chunks
 218  
      * @param bufSize Buffer size
 219  
      */
 220  9
     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufSize) {
 221  9
         this.file = file;
 222  9
         this.delayMillis = delayMillis;
 223  9
         this.end = end;
 224  
 
 225  9
         this.inbuf = new byte[bufSize];
 226  
 
 227  
         // Save and prepare the listener
 228  9
         this.listener = listener;
 229  9
         listener.init(this);
 230  9
         this.reOpen = reOpen;
 231  9
     }
 232  
 
 233  
     /**
 234  
      * Creates and starts a Tailer for the given file.
 235  
      *
 236  
      * @param file the file to follow.
 237  
      * @param listener the TailerListener to use.
 238  
      * @param delayMillis the delay between checks of the file for new content in milliseconds.
 239  
      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
 240  
      * @param bufSize buffer size.
 241  
      * @return The new tailer
 242  
      */
 243  
     public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufSize) {
 244  1
         return create(file, listener, delayMillis, end, false, bufSize);
 245  
     }
 246  
 
 247  
     /**
 248  
      * Creates and starts a Tailer for the given file.
 249  
      *
 250  
      * @param file the file to follow.
 251  
      * @param listener the TailerListener to use.
 252  
      * @param delayMillis the delay between checks of the file for new content in milliseconds.
 253  
      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
 254  
      * @param reOpen whether to close/reopen the file between chunks
 255  
      * @param bufSize buffer size.
 256  
      * @return The new tailer
 257  
      */
 258  
     public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen,
 259  
             final int bufSize) {
 260  1
         final Tailer tailer = new Tailer(file, listener, delayMillis, end, reOpen, bufSize);
 261  1
         final Thread thread = new Thread(tailer);
 262  1
         thread.setDaemon(true);
 263  1
         thread.start();
 264  1
         return tailer;
 265  
     }
 266  
 
 267  
     /**
 268  
      * Creates and starts a Tailer for the given file with default buffer size.
 269  
      *
 270  
      * @param file the file to follow.
 271  
      * @param listener the TailerListener to use.
 272  
      * @param delayMillis the delay between checks of the file for new content in milliseconds.
 273  
      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
 274  
      * @return The new tailer
 275  
      */
 276  
     public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
 277  1
         return create(file, listener, delayMillis, end, DEFAULT_BUFSIZE);
 278  
     }
 279  
 
 280  
     /**
 281  
      * Creates and starts a Tailer for the given file with default buffer size.
 282  
      *
 283  
      * @param file the file to follow.
 284  
      * @param listener the TailerListener to use.
 285  
      * @param delayMillis the delay between checks of the file for new content in milliseconds.
 286  
      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
 287  
      * @param reOpen whether to close/reopen the file between chunks
 288  
      * @return The new tailer
 289  
      */
 290  
     public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) {
 291  0
         return create(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE);
 292  
     }
 293  
 
 294  
     /**
 295  
      * Creates and starts a Tailer for the given file, starting at the beginning of the file
 296  
      *
 297  
      * @param file the file to follow.
 298  
      * @param listener the TailerListener to use.
 299  
      * @param delayMillis the delay between checks of the file for new content in milliseconds.
 300  
      * @return The new tailer
 301  
      */
 302  
     public static Tailer create(final File file, final TailerListener listener, final long delayMillis) {
 303  0
         return create(file, listener, delayMillis, false);
 304  
     }
 305  
 
 306  
     /**
 307  
      * Creates and starts a Tailer for the given file, starting at the beginning of the file
 308  
      * with the default delay of 1.0s
 309  
      *
 310  
      * @param file the file to follow.
 311  
      * @param listener the TailerListener to use.
 312  
      * @return The new tailer
 313  
      */
 314  
     public static Tailer create(final File file, final TailerListener listener) {
 315  0
         return create(file, listener, DEFAULT_DELAY_MILLIS, false);
 316  
     }
 317  
 
 318  
     /**
 319  
      * Return the file.
 320  
      *
 321  
      * @return the file
 322  
      */
 323  
     public File getFile() {
 324  0
         return file;
 325  
     }
 326  
 
 327  
     /**
 328  
      * Gets whether to keep on running.
 329  
      *
 330  
      * @return whether to keep on running.
 331  
      * @since 2.5
 332  
      */
 333  
     protected boolean getRun() {
 334  1215
         return run;
 335  
     }
 336  
 
 337  
     /**
 338  
      * Return the delay in milliseconds.
 339  
      *
 340  
      * @return the delay in milliseconds.
 341  
      */
 342  
     public long getDelay() {
 343  0
         return delayMillis;
 344  
     }
 345  
 
 346  
     /**
 347  
      * Follows changes in the file, calling the TailerListener's handle method for each new line.
 348  
      */
 349  
     public void run() {
 350  9
         RandomAccessFile reader = null;
 351  
         try {
 352  9
             long last = 0; // The last time the file was checked for changes
 353  9
             long position = 0; // position within the file
 354  
             // Open the file
 355  17
             while (getRun() && reader == null) {
 356  
                 try {
 357  9
                     reader = new RandomAccessFile(file, RAF_MODE);
 358  3
                 } catch (final FileNotFoundException e) {
 359  3
                     listener.fileNotFound();
 360  6
                 }
 361  9
                 if (reader == null) {
 362  3
                     Thread.sleep(delayMillis);
 363  
                 } else {
 364  
                     // The current position in the file
 365  6
                     position = end ? file.length() : 0;
 366  6
                     last = file.lastModified();
 367  6
                     reader.seek(position);
 368  
                 }
 369  
             }
 370  79
             while (getRun()) {
 371  73
                 final boolean newer = FileUtils.isFileNewer(file, last); // IO-279, must be done first
 372  
                 // Check the file length to see if it was rotated
 373  73
                 final long length = file.length();
 374  73
                 if (length < position) {
 375  
                     // File was rotated
 376  1
                     listener.fileRotated();
 377  
                     // Reopen the reader after rotation
 378  
                     try {
 379  
                         // Ensure that the old file is closed iff we re-open it successfully
 380  1
                         final RandomAccessFile save = reader;
 381  1
                         reader = new RandomAccessFile(file, RAF_MODE);
 382  
                         // At this point, we're sure that the old file is rotated
 383  
                         // Finish scanning the old file and then we'll start with the new one
 384  
                         try {
 385  1
                             readLines(save);
 386  0
                         }  catch (IOException ioe) {
 387  0
                             listener.handle(ioe);
 388  1
                         }
 389  1
                         position = 0;
 390  
                         // close old file explicitly rather than relying on GC picking up previous RAF
 391  1
                         IOUtils.closeQuietly(save);
 392  0
                     } catch (final FileNotFoundException e) {
 393  
                         // in this case we continue to use the previous reader and position values
 394  0
                         listener.fileNotFound();
 395  1
                     }
 396  0
                     continue;
 397  
                 } else {
 398  
                     // File was not rotated
 399  
                     // See if the file needs to be read again
 400  72
                     if (length > position) {
 401  
                         // The file has more content than it did last time
 402  19
                         position = readLines(reader);
 403  19
                         last = file.lastModified();
 404  53
                     } else if (newer) {
 405  
                         /*
 406  
                          * This can happen if the file is truncated or overwritten with the exact same length of
 407  
                          * information. In cases like this, the file position needs to be reset
 408  
                          */
 409  1
                         position = 0;
 410  1
                         reader.seek(position); // cannot be null here
 411  
 
 412  
                         // Now we can read new lines
 413  1
                         position = readLines(reader);
 414  1
                         last = file.lastModified();
 415  
                     }
 416  
                 }
 417  72
                 if (reOpen) {
 418  56
                     IOUtils.closeQuietly(reader);
 419  
                 }
 420  72
                 Thread.sleep(delayMillis);
 421  70
                 if (getRun() && reOpen) {
 422  54
                     reader = new RandomAccessFile(file, RAF_MODE);
 423  54
                     reader.seek(position);
 424  
                 }
 425  70
             }
 426  3
         } catch (final InterruptedException e) {
 427  3
             Thread.currentThread().interrupt();
 428  3
             stop(e);
 429  0
         } catch (final Exception e) {
 430  0
             stop(e);
 431  
         } finally {
 432  9
             IOUtils.closeQuietly(reader);
 433  9
         }
 434  9
     }
 435  
 
 436  
     private void stop(final Exception e) {
 437  3
         listener.handle(e);
 438  3
         stop();
 439  3
     }
 440  
 
 441  
     /**
 442  
      * Allows the tailer to complete its current loop and return.
 443  
      */
 444  
     public void stop() {
 445  12
         this.run = false;
 446  12
     }
 447  
 
 448  
     /**
 449  
      * Read new lines.
 450  
      *
 451  
      * @param reader The file to read
 452  
      * @return The new position after the lines have been read
 453  
      * @throws java.io.IOException if an I/O error occurs.
 454  
      */
 455  
     private long readLines(final RandomAccessFile reader) throws IOException {
 456  
         // Make explicit that the default charset is being used here
 457  21
         Charset cset = Charset.defaultCharset();
 458  21
         ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64);
 459  21
         long pos = reader.getFilePointer();
 460  21
         long rePos = pos; // position to re-read
 461  
         int num;
 462  21
         boolean seenCR = false;
 463  1049
         while (getRun() && ((num = reader.read(inbuf)) != -1)) {
 464  4102868
             for (int i = 0; i < num; i++) {
 465  4101840
                 final byte ch = inbuf[i];
 466  4101840
                 switch (ch) {
 467  
                 case '\n':
 468  100061
                     seenCR = false; // swallow CR before LF
 469  100061
                     listener.handle(new String(lineBuf.toByteArray(), cset));
 470  100061
                     lineBuf.reset();
 471  100061
                     rePos = pos + i + 1;
 472  100061
                     break;
 473  
                 case '\r':
 474  4
                     if (seenCR) {
 475  1
                         lineBuf.write('\r');
 476  
                     }
 477  4
                     seenCR = true;
 478  4
                     break;
 479  
                 default:
 480  4001775
                     if (seenCR) {
 481  2
                         seenCR = false; // swallow final CR
 482  2
                         listener.handle(new String(lineBuf.toByteArray(), cset));
 483  2
                         lineBuf.reset();
 484  2
                         rePos = pos + i + 1;
 485  
                     }
 486  4001775
                     lineBuf.write(ch);
 487  
                 }
 488  
             }
 489  1028
             pos = reader.getFilePointer();
 490  
         }
 491  21
         IOUtils.closeQuietly(lineBuf); // not strictly necessary
 492  21
         reader.seek(rePos); // Ensure we can re-read if necessary
 493  21
         return rePos;
 494  
     }
 495  
 
 496  
 }