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