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