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