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.CR; 20 import static org.apache.commons.io.IOUtils.EOF; 21 import static org.apache.commons.io.IOUtils.LF; 22 23 import java.io.ByteArrayOutputStream; 24 import java.io.Closeable; 25 import java.io.File; 26 import java.io.FileNotFoundException; 27 import java.io.IOException; 28 import java.io.RandomAccessFile; 29 import java.nio.charset.Charset; 30 import java.nio.file.Files; 31 import java.nio.file.LinkOption; 32 import java.nio.file.Path; 33 import java.nio.file.attribute.FileTime; 34 import java.time.Duration; 35 import java.util.Arrays; 36 import java.util.Objects; 37 38 import org.apache.commons.io.IOUtils; 39 import org.apache.commons.io.ThreadUtils; 40 import org.apache.commons.io.file.PathUtils; 41 import org.apache.commons.io.file.attribute.FileTimes; 42 43 /** 44 * Simple implementation of the UNIX "tail -f" functionality. 45 * 46 * <h2>1. Create a TailerListener implementation</h2> 47 * <p> 48 * First you need to create a {@link TailerListener} implementation; ({@link TailerListenerAdapter} is provided for 49 * convenience so that you don't have to implement every method). 50 * </p> 51 * 52 * <p> 53 * For example: 54 * </p> 55 * 56 * <pre> 57 * public class MyTailerListener extends TailerListenerAdapter { 58 * public void handle(String line) { 59 * System.out.println(line); 60 * } 61 * } 62 * </pre> 63 * 64 * <h2>2. Using a Tailer</h2> 65 * 66 * <p> 67 * You can create and use a Tailer in one of four ways: 68 * </p> 69 * <ul> 70 * <li>Using a {@link Builder}</li> 71 * <li>Using one of the static helper methods: 72 * <ul> 73 * <li>{@link Tailer#create(File, TailerListener)}</li> 74 * <li>{@link Tailer#create(File, TailerListener, long)}</li> 75 * <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li> 76 * </ul> 77 * </li> 78 * <li>Using an {@link java.util.concurrent.Executor}</li> 79 * <li>Using a {@link Thread}</li> 80 * </ul> 81 * 82 * <p> 83 * An example of each is shown below. 84 * </p> 85 * 86 * <h3>2.1 Using a Builder</h3> 87 * 88 * <pre> 89 * TailerListener listener = new MyTailerListener(); 90 * Tailer tailer = new Tailer.Builder(file, listener).withDelayDuration(delay).build(); 91 * </pre> 92 * 93 * <h3>2.2 Using the static helper method</h3> 94 * 95 * <pre> 96 * TailerListener listener = new MyTailerListener(); 97 * Tailer tailer = Tailer.create(file, listener, delay); 98 * </pre> 99 * 100 * <h3>2.3 Using an Executor</h3> 101 * 102 * <pre> 103 * TailerListener listener = new MyTailerListener(); 104 * Tailer tailer = new Tailer(file, listener, delay); 105 * 106 * // stupid executor impl. for demo purposes 107 * Executor executor = new Executor() { 108 * public void execute(Runnable command) { 109 * command.run(); 110 * } 111 * }; 112 * 113 * executor.execute(tailer); 114 * </pre> 115 * 116 * 117 * <h3>2.4 Using a Thread</h3> 118 * 119 * <pre> 120 * TailerListener listener = new MyTailerListener(); 121 * Tailer tailer = new Tailer(file, listener, delay); 122 * Thread thread = new Thread(tailer); 123 * thread.setDaemon(true); // optional 124 * thread.start(); 125 * </pre> 126 * 127 * <h2>3. Stopping a Tailer</h2> 128 * <p> 129 * Remember to stop the tailer when you have done with it: 130 * </p> 131 * 132 * <pre> 133 * tailer.stop(); 134 * </pre> 135 * 136 * <h2>4. Interrupting a Tailer</h2> 137 * <p> 138 * You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}. 139 * </p> 140 * 141 * <pre> 142 * thread.interrupt(); 143 * </pre> 144 * <p> 145 * If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}. 146 * </p> 147 * <p> 148 * The file is read using the default Charset; this can be overridden if necessary. 149 * </p> 150 * 151 * @see TailerListener 152 * @see TailerListenerAdapter 153 * @since 2.0 154 * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()}. 155 * @since 2.12.0 Add {@link Tailable} and {@link RandomAccessResourceBridge} interfaces to tail of files accessed using 156 * alternative libraries such as jCIFS or <a href="https://commons.apache.org/proper/commons-vfs/">Apache Commons 157 * VFS</a>. 158 */ 159 public class Tailer implements Runnable, AutoCloseable { 160 161 /** 162 * Builds a {@link Tailer} with default values. 163 * 164 * @since 2.12.0 165 */ 166 public static class Builder { 167 168 private final Tailable tailable; 169 private final TailerListener tailerListener; 170 private Charset charset = DEFAULT_CHARSET; 171 private int bufferSize = IOUtils.DEFAULT_BUFFER_SIZE; 172 private Duration delayDuration = Duration.ofMillis(DEFAULT_DELAY_MILLIS); 173 private boolean end; 174 private boolean reOpen; 175 private boolean startThread = true; 176 177 /** 178 * Creates a builder. 179 * 180 * @param file the file to follow. 181 * @param listener the TailerListener to use. 182 */ Builder(final File file, final TailerListener listener)183 public Builder(final File file, final TailerListener listener) { 184 this(file.toPath(), listener); 185 } 186 187 /** 188 * Creates a builder. 189 * 190 * @param file the file to follow. 191 * @param listener the TailerListener to use. 192 */ Builder(final Path file, final TailerListener listener)193 public Builder(final Path file, final TailerListener listener) { 194 this(new TailablePath(file), listener); 195 } 196 197 /** 198 * Creates a builder. 199 * 200 * @param tailable the tailable to follow. 201 * @param tailerListener the TailerListener to use. 202 */ Builder(final Tailable tailable, final TailerListener tailerListener)203 public Builder(final Tailable tailable, final TailerListener tailerListener) { 204 this.tailable = Objects.requireNonNull(tailable, "tailable"); 205 this.tailerListener = Objects.requireNonNull(tailerListener, "tailerListener"); 206 } 207 208 /** 209 * Builds and starts a new configured instance. 210 * 211 * @return a new configured instance. 212 */ build()213 public Tailer build() { 214 final Tailer tailer = new Tailer(tailable, charset, tailerListener, delayDuration, end, reOpen, bufferSize); 215 if (startThread) { 216 final Thread thread = new Thread(tailer); 217 thread.setDaemon(true); 218 thread.start(); 219 } 220 return tailer; 221 } 222 223 /** 224 * Sets the buffer size. 225 * 226 * @param bufferSize Buffer size. 227 * @return Builder with specific buffer size. 228 */ withBufferSize(final int bufferSize)229 public Builder withBufferSize(final int bufferSize) { 230 this.bufferSize = bufferSize; 231 return this; 232 } 233 234 /** 235 * Sets the Charset. 236 * 237 * @param charset the Charset to be used for reading the file. 238 * @return Builder with specific Charset. 239 */ withCharset(final Charset charset)240 public Builder withCharset(final Charset charset) { 241 this.charset = Objects.requireNonNull(charset, "charset"); 242 return this; 243 } 244 245 /** 246 * Sets the delay duration. 247 * 248 * @param delayDuration the delay between checks of the file for new content. 249 * @return Builder with specific delay duration. 250 */ withDelayDuration(final Duration delayDuration)251 public Builder withDelayDuration(final Duration delayDuration) { 252 this.delayDuration = Objects.requireNonNull(delayDuration, "delayDuration"); 253 return this; 254 } 255 256 /** 257 * Sets the re-open behavior. 258 * 259 * @param reOpen whether to close/reopen the file between chunks 260 * @return Builder with specific re-open behavior 261 */ withReOpen(final boolean reOpen)262 public Builder withReOpen(final boolean reOpen) { 263 this.reOpen = reOpen; 264 return this; 265 } 266 267 /** 268 * Sets the daemon thread startup behavior. 269 * 270 * @param startThread whether to create a daemon thread automatically. 271 * @return Builder with specific daemon thread startup behavior. 272 */ withStartThread(final boolean startThread)273 public Builder withStartThread(final boolean startThread) { 274 this.startThread = startThread; 275 return this; 276 } 277 278 /** 279 * Sets the tail start behavior. 280 * 281 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 282 * @return Builder with specific tail start behavior. 283 */ withTailFromEnd(final boolean end)284 public Builder withTailFromEnd(final boolean end) { 285 this.end = end; 286 return this; 287 } 288 } 289 290 /** 291 * Bridges random access to a {@link RandomAccessFile}. 292 */ 293 private static final class RandomAccessFileBridge implements RandomAccessResourceBridge { 294 295 private final RandomAccessFile randomAccessFile; 296 RandomAccessFileBridge(final File file, final String mode)297 private RandomAccessFileBridge(final File file, final String mode) throws FileNotFoundException { 298 randomAccessFile = new RandomAccessFile(file, mode); 299 } 300 301 @Override close()302 public void close() throws IOException { 303 randomAccessFile.close(); 304 } 305 306 @Override getPointer()307 public long getPointer() throws IOException { 308 return randomAccessFile.getFilePointer(); 309 } 310 311 @Override read(final byte[] b)312 public int read(final byte[] b) throws IOException { 313 return randomAccessFile.read(b); 314 } 315 316 @Override seek(final long position)317 public void seek(final long position) throws IOException { 318 randomAccessFile.seek(position); 319 } 320 321 } 322 323 /** 324 * Bridges access to a resource for random access, normally a file. Allows substitution of remote files for example 325 * using jCIFS. 326 * 327 * @since 2.12.0 328 */ 329 public interface RandomAccessResourceBridge extends Closeable { 330 331 /** 332 * Gets the current offset in this tailable. 333 * 334 * @return the offset from the beginning of the tailable, in bytes, at which the next read or write occurs. 335 * @throws IOException if an I/O error occurs. 336 */ getPointer()337 long getPointer() throws IOException; 338 339 /** 340 * Reads up to {@code b.length} bytes of data from this tailable into an array of bytes. This method blocks until at 341 * least one byte of input is available. 342 * 343 * @param b the buffer into which the data is read. 344 * @return the total number of bytes read into the buffer, or {@code -1} if there is no more data because the end of 345 * this tailable has been reached. 346 * @throws IOException If the first byte cannot be read for any reason other than end of tailable, or if the random 347 * access tailable has been closed, or if some other I/O error occurs. 348 */ read(final byte[] b)349 int read(final byte[] b) throws IOException; 350 351 /** 352 * Sets the file-pointer offset, measured from the beginning of this tailable, at which the next read or write occurs. 353 * The offset may be set beyond the end of the tailable. Setting the offset beyond the end of the tailable does not 354 * change the tailable length. The tailable length will change only by writing after the offset has been set beyond the 355 * end of the tailable. 356 * 357 * @param pos the offset position, measured in bytes from the beginning of the tailable, at which to set the tailable 358 * pointer. 359 * @throws IOException if {@code pos} is less than {@code 0} or if an I/O error occurs. 360 */ seek(final long pos)361 void seek(final long pos) throws IOException; 362 } 363 364 /** 365 * A tailable resource like a file. 366 * 367 * @since 2.12.0 368 */ 369 public interface Tailable { 370 371 /** 372 * Creates a random access file stream to read. 373 * 374 * @param mode the access mode, by default this is for {@link RandomAccessFile}. 375 * @return a random access file stream to read. 376 * @throws FileNotFoundException if the tailable object does not exist. 377 */ getRandomAccess(final String mode)378 RandomAccessResourceBridge getRandomAccess(final String mode) throws FileNotFoundException; 379 380 /** 381 * Tests if this tailable is newer than the specified {@link FileTime}. 382 * 383 * @param fileTime the file time reference. 384 * @return true if the {@link File} exists and has been modified after the given {@link FileTime}. 385 * @throws IOException if an I/O error occurs. 386 */ isNewer(final FileTime fileTime)387 boolean isNewer(final FileTime fileTime) throws IOException; 388 389 /** 390 * Gets the last modification {@link FileTime}. 391 * 392 * @return See {@link java.nio.file.Files#getLastModifiedTime(Path, LinkOption...)}. 393 * @throws IOException if an I/O error occurs. 394 */ lastModifiedFileTime()395 FileTime lastModifiedFileTime() throws IOException; 396 397 /** 398 * Gets the size of this tailable. 399 * 400 * @return The size, in bytes, of this tailable, or {@code 0} if the file does not exist. Some operating systems may 401 * return {@code 0} for path names denoting system-dependent entities such as devices or pipes. 402 * @throws IOException if an I/O error occurs. 403 */ size()404 long size() throws IOException; 405 } 406 407 /** 408 * A tailable for a file {@link Path}. 409 */ 410 private static final class TailablePath implements Tailable { 411 412 private final Path path; 413 private final LinkOption[] linkOptions; 414 TailablePath(final Path path, final LinkOption... linkOptions)415 private TailablePath(final Path path, final LinkOption... linkOptions) { 416 this.path = Objects.requireNonNull(path, "path"); 417 this.linkOptions = linkOptions; 418 } 419 getPath()420 Path getPath() { 421 return path; 422 } 423 424 @Override getRandomAccess(final String mode)425 public RandomAccessResourceBridge getRandomAccess(final String mode) throws FileNotFoundException { 426 return new RandomAccessFileBridge(path.toFile(), mode); 427 } 428 429 @Override isNewer(final FileTime fileTime)430 public boolean isNewer(final FileTime fileTime) throws IOException { 431 return PathUtils.isNewer(path, fileTime, linkOptions); 432 } 433 434 @Override lastModifiedFileTime()435 public FileTime lastModifiedFileTime() throws IOException { 436 return Files.getLastModifiedTime(path, linkOptions); 437 } 438 439 @Override size()440 public long size() throws IOException { 441 return Files.size(path); 442 } 443 444 @Override toString()445 public String toString() { 446 return "TailablePath [file=" + path + ", linkOptions=" + Arrays.toString(linkOptions) + "]"; 447 } 448 } 449 450 private static final int DEFAULT_DELAY_MILLIS = 1000; 451 452 private static final String RAF_READ_ONLY_MODE = "r"; 453 454 // The default charset used for reading files 455 private static final Charset DEFAULT_CHARSET = Charset.defaultCharset(); 456 457 /** 458 * Creates and starts a Tailer for the given file. 459 * 460 * @param file the file to follow. 461 * @param charset the character set to use for reading the file. 462 * @param listener the TailerListener to use. 463 * @param delayMillis the delay between checks of the file for new content in milliseconds. 464 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 465 * @param reOpen whether to close/reopen the file between chunks. 466 * @param bufferSize buffer size. 467 * @return The new tailer. 468 * @deprecated Use {@link Builder}. 469 */ 470 @Deprecated create(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufferSize)471 public static Tailer create(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, 472 final boolean reOpen, final int bufferSize) { 473 //@formatter:off 474 return new Builder(file, listener) 475 .withCharset(charset) 476 .withDelayDuration(Duration.ofMillis(delayMillis)) 477 .withTailFromEnd(end) 478 .withReOpen(reOpen) 479 .withBufferSize(bufferSize) 480 .build(); 481 //@formatter:on 482 } 483 484 /** 485 * Creates and starts a Tailer for the given file, starting at the beginning of the file with the default delay of 1.0s 486 * 487 * @param file the file to follow. 488 * @param listener the TailerListener to use. 489 * @return The new tailer. 490 * @deprecated Use {@link Builder}. 491 */ 492 @Deprecated create(final File file, final TailerListener listener)493 public static Tailer create(final File file, final TailerListener listener) { 494 return new Builder(file, listener).build(); 495 } 496 497 /** 498 * Creates and starts a Tailer for the given file, starting at the beginning of the file 499 * 500 * @param file the file to follow. 501 * @param listener the TailerListener to use. 502 * @param delayMillis the delay between checks of the file for new content in milliseconds. 503 * @return The new tailer. 504 * @deprecated Use {@link Builder}. 505 */ 506 @Deprecated create(final File file, final TailerListener listener, final long delayMillis)507 public static Tailer create(final File file, final TailerListener listener, final long delayMillis) { 508 //@formatter:off 509 return new Builder(file, listener) 510 .withDelayDuration(Duration.ofMillis(delayMillis)) 511 .build(); 512 //@formatter:on 513 } 514 515 /** 516 * Creates and starts a Tailer for the given file with default buffer size. 517 * 518 * @param file the file to follow. 519 * @param listener the TailerListener to use. 520 * @param delayMillis the delay between checks of the file for new content in milliseconds. 521 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 522 * @return The new tailer. 523 * @deprecated Use {@link Builder}. 524 */ 525 @Deprecated create(final File file, final TailerListener listener, final long delayMillis, final boolean end)526 public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end) { 527 //@formatter:off 528 return new Builder(file, listener) 529 .withDelayDuration(Duration.ofMillis(delayMillis)) 530 .withTailFromEnd(end) 531 .build(); 532 //@formatter:on 533 } 534 535 /** 536 * Creates and starts a Tailer for the given file with default buffer size. 537 * 538 * @param file the file to follow. 539 * @param listener the TailerListener to use. 540 * @param delayMillis the delay between checks of the file for new content in milliseconds. 541 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 542 * @param reOpen whether to close/reopen the file between chunks. 543 * @return The new tailer. 544 * @deprecated Use {@link Builder}. 545 */ 546 @Deprecated create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen)547 public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) { 548 //@formatter:off 549 return new Builder(file, listener) 550 .withDelayDuration(Duration.ofMillis(delayMillis)) 551 .withTailFromEnd(end) 552 .withReOpen(reOpen) 553 .build(); 554 //@formatter:on 555 } 556 557 /** 558 * Creates and starts a Tailer for the given file. 559 * 560 * @param file the file to follow. 561 * @param listener the TailerListener to use. 562 * @param delayMillis the delay between checks of the file for new content in milliseconds. 563 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 564 * @param reOpen whether to close/reopen the file between chunks. 565 * @param bufferSize buffer size. 566 * @return The new tailer. 567 * @deprecated Use {@link Builder}. 568 */ 569 @Deprecated create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufferSize)570 public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, 571 final int bufferSize) { 572 //@formatter:off 573 return new Builder(file, listener) 574 .withDelayDuration(Duration.ofMillis(delayMillis)) 575 .withTailFromEnd(end) 576 .withReOpen(reOpen) 577 .withBufferSize(bufferSize) 578 .build(); 579 //@formatter:on 580 } 581 582 /** 583 * Creates and starts a Tailer for the given file. 584 * 585 * @param file the file to follow. 586 * @param listener the TailerListener to use. 587 * @param delayMillis the delay between checks of the file for new content in milliseconds. 588 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 589 * @param bufferSize buffer size. 590 * @return The new tailer. 591 * @deprecated Use {@link Builder}. 592 */ 593 @Deprecated create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufferSize)594 public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufferSize) { 595 //@formatter:off 596 return new Builder(file, listener) 597 .withDelayDuration(Duration.ofMillis(delayMillis)) 598 .withTailFromEnd(end) 599 .withBufferSize(bufferSize) 600 .build(); 601 //@formatter:on 602 } 603 604 /** 605 * Buffer on top of RandomAccessResourceBridge. 606 */ 607 private final byte[] inbuf; 608 609 /** 610 * The file which will be tailed. 611 */ 612 private final Tailable tailable; 613 614 /** 615 * The character set that will be used to read the file. 616 */ 617 private final Charset charset; 618 619 /** 620 * The amount of time to wait for the file to be updated. 621 */ 622 private final Duration delayDuration; 623 624 /** 625 * Whether to tail from the end or start of file 626 */ 627 private final boolean tailAtEnd; 628 629 /** 630 * The listener to notify of events when tailing. 631 */ 632 private final TailerListener listener; 633 634 /** 635 * Whether to close and reopen the file whilst waiting for more input. 636 */ 637 private final boolean reOpen; 638 639 /** 640 * The tailer will run as long as this value is true. 641 */ 642 private volatile boolean run = true; 643 644 /** 645 * Creates a Tailer for the given file, with a specified buffer size. 646 * 647 * @param file the file to follow. 648 * @param charset the Charset to be used for reading the file 649 * @param listener the TailerListener to use. 650 * @param delayMillis the delay between checks of the file for new content in milliseconds. 651 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 652 * @param reOpen if true, close and reopen the file between reading chunks 653 * @param bufSize Buffer size 654 * @deprecated Use {@link Builder}. 655 */ 656 @Deprecated Tailer(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufSize)657 public Tailer(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, 658 final int bufSize) { 659 this(new TailablePath(file.toPath()), charset, listener, Duration.ofMillis(delayMillis), end, reOpen, bufSize); 660 } 661 662 /** 663 * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s. 664 * 665 * @param file The file to follow. 666 * @param listener the TailerListener to use. 667 * @deprecated Use {@link Builder}. 668 */ 669 @Deprecated Tailer(final File file, final TailerListener listener)670 public Tailer(final File file, final TailerListener listener) { 671 this(file, listener, DEFAULT_DELAY_MILLIS); 672 } 673 674 /** 675 * Creates a Tailer for the given file, starting from the beginning. 676 * 677 * @param file the file to follow. 678 * @param listener the TailerListener to use. 679 * @param delayMillis the delay between checks of the file for new content in milliseconds. 680 * @deprecated Use {@link Builder}. 681 */ 682 @Deprecated Tailer(final File file, final TailerListener listener, final long delayMillis)683 public Tailer(final File file, final TailerListener listener, final long delayMillis) { 684 this(file, listener, delayMillis, false); 685 } 686 687 /** 688 * Creates a Tailer for the given file, with a delay other than the default 1.0s. 689 * 690 * @param file the file to follow. 691 * @param listener the TailerListener to use. 692 * @param delayMillis the delay between checks of the file for new content in milliseconds. 693 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 694 * @deprecated Use {@link Builder}. 695 */ 696 @Deprecated Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end)697 public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) { 698 this(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE); 699 } 700 701 /** 702 * Creates a Tailer for the given file, with a delay other than the default 1.0s. 703 * 704 * @param file the file to follow. 705 * @param listener the TailerListener to use. 706 * @param delayMillis the delay between checks of the file for new content in milliseconds. 707 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 708 * @param reOpen if true, close and reopen the file between reading chunks 709 * @deprecated Use {@link Builder}. 710 */ 711 @Deprecated Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen)712 public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) { 713 this(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE); 714 } 715 716 /** 717 * Creates a Tailer for the given file, with a specified buffer size. 718 * 719 * @param file the file to follow. 720 * @param listener the TailerListener to use. 721 * @param delayMillis the delay between checks of the file for new content in milliseconds. 722 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 723 * @param reOpen if true, close and reopen the file between reading chunks 724 * @param bufferSize Buffer size 725 * @deprecated Use {@link Builder}. 726 */ 727 @Deprecated Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufferSize)728 public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufferSize) { 729 this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufferSize); 730 } 731 732 /** 733 * Creates a Tailer for the given file, with a specified buffer size. 734 * 735 * @param file the file to follow. 736 * @param listener the TailerListener to use. 737 * @param delayMillis the delay between checks of the file for new content in milliseconds. 738 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 739 * @param bufferSize Buffer size 740 * @deprecated Use {@link Builder}. 741 */ 742 @Deprecated Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufferSize)743 public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufferSize) { 744 this(file, listener, delayMillis, end, false, bufferSize); 745 } 746 747 /** 748 * Creates a Tailer for the given file, with a specified buffer size. 749 * 750 * @param tailable the file to follow. 751 * @param charset the Charset to be used for reading the file 752 * @param listener the TailerListener to use. 753 * @param delayDuration the delay between checks of the file for new content in milliseconds. 754 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 755 * @param reOpen if true, close and reopen the file between reading chunks 756 * @param bufferSize Buffer size 757 */ Tailer(final Tailable tailable, final Charset charset, final TailerListener listener, final Duration delayDuration, final boolean end, final boolean reOpen, final int bufferSize)758 private Tailer(final Tailable tailable, final Charset charset, final TailerListener listener, final Duration delayDuration, final boolean end, 759 final boolean reOpen, final int bufferSize) { 760 this.tailable = tailable; 761 this.delayDuration = delayDuration; 762 this.tailAtEnd = end; 763 this.inbuf = IOUtils.byteArray(bufferSize); 764 765 // Save and prepare the listener 766 this.listener = listener; 767 listener.init(this); 768 this.reOpen = reOpen; 769 this.charset = charset; 770 } 771 772 /** 773 * Requests the tailer to complete its current loop and return. 774 */ 775 @Override close()776 public void close() { 777 this.run = false; 778 } 779 780 /** 781 * Gets the delay in milliseconds. 782 * 783 * @return the delay in milliseconds. 784 * @deprecated Use {@link #getDelayDuration()}. 785 */ 786 @Deprecated getDelay()787 public long getDelay() { 788 return delayDuration.toMillis(); 789 } 790 791 /** 792 * Gets the delay Duration. 793 * 794 * @return the delay Duration. 795 * @since 2.12.0 796 */ getDelayDuration()797 public Duration getDelayDuration() { 798 return delayDuration; 799 } 800 801 /** 802 * Gets the file. 803 * 804 * @return the file 805 * @throws IllegalStateException if constructed using a user provided {@link Tailable} implementation 806 */ getFile()807 public File getFile() { 808 if (tailable instanceof TailablePath) { 809 return ((TailablePath) tailable).getPath().toFile(); 810 } 811 throw new IllegalStateException("Cannot extract java.io.File from " + tailable.getClass().getName()); 812 } 813 814 /** 815 * Gets whether to keep on running. 816 * 817 * @return whether to keep on running. 818 * @since 2.5 819 */ getRun()820 protected boolean getRun() { 821 return run; 822 } 823 824 /** 825 * Gets the Tailable. 826 * 827 * @return the Tailable 828 * @since 2.12.0 829 */ getTailable()830 public Tailable getTailable() { 831 return tailable; 832 } 833 834 /** 835 * Reads new lines. 836 * 837 * @param reader The file to read 838 * @return The new position after the lines have been read 839 * @throws IOException if an I/O error occurs. 840 */ readLines(final RandomAccessResourceBridge reader)841 private long readLines(final RandomAccessResourceBridge reader) throws IOException { 842 try (ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64)) { 843 long pos = reader.getPointer(); 844 long rePos = pos; // position to re-read 845 int num; 846 boolean seenCR = false; 847 while (getRun() && (num = reader.read(inbuf)) != EOF) { 848 for (int i = 0; i < num; i++) { 849 final byte ch = inbuf[i]; 850 switch (ch) { 851 case LF: 852 seenCR = false; // swallow CR before LF 853 listener.handle(new String(lineBuf.toByteArray(), charset)); 854 lineBuf.reset(); 855 rePos = pos + i + 1; 856 break; 857 case CR: 858 if (seenCR) { 859 lineBuf.write(CR); 860 } 861 seenCR = true; 862 break; 863 default: 864 if (seenCR) { 865 seenCR = false; // swallow final CR 866 listener.handle(new String(lineBuf.toByteArray(), charset)); 867 lineBuf.reset(); 868 rePos = pos + i + 1; 869 } 870 lineBuf.write(ch); 871 } 872 } 873 pos = reader.getPointer(); 874 } 875 876 reader.seek(rePos); // Ensure we can re-read if necessary 877 878 if (listener instanceof TailerListenerAdapter) { 879 ((TailerListenerAdapter) listener).endOfFileReached(); 880 } 881 882 return rePos; 883 } 884 } 885 886 /** 887 * Follows changes in the file, calling {@link TailerListener#handle(String)} with each new line. 888 */ 889 @Override run()890 public void run() { 891 RandomAccessResourceBridge reader = null; 892 try { 893 FileTime last = FileTimes.EPOCH; // The last time the file was checked for changes 894 long position = 0; // position within the file 895 // Open the file 896 while (getRun() && reader == null) { 897 try { 898 reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE); 899 } catch (final FileNotFoundException e) { 900 listener.fileNotFound(); 901 } 902 if (reader == null) { 903 ThreadUtils.sleep(delayDuration); 904 } else { 905 // The current position in the file 906 position = tailAtEnd ? tailable.size() : 0; 907 last = tailable.lastModifiedFileTime(); 908 reader.seek(position); 909 } 910 } 911 while (getRun()) { 912 final boolean newer = tailable.isNewer(last); // IO-279, must be done first 913 // Check the file length to see if it was rotated 914 final long length = tailable.size(); 915 if (length < position) { 916 // File was rotated 917 listener.fileRotated(); 918 // Reopen the reader after rotation ensuring that the old file is closed iff we re-open it 919 // successfully 920 try (RandomAccessResourceBridge save = reader) { 921 reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE); 922 // At this point, we're sure that the old file is rotated 923 // Finish scanning the old file and then we'll start with the new one 924 try { 925 readLines(save); 926 } catch (final IOException ioe) { 927 listener.handle(ioe); 928 } 929 position = 0; 930 } catch (final FileNotFoundException e) { 931 // in this case we continue to use the previous reader and position values 932 listener.fileNotFound(); 933 ThreadUtils.sleep(delayDuration); 934 } 935 continue; 936 } 937 // File was not rotated 938 // See if the file needs to be read again 939 if (length > position) { 940 // The file has more content than it did last time 941 position = readLines(reader); 942 last = tailable.lastModifiedFileTime(); 943 } else if (newer) { 944 /* 945 * This can happen if the file is truncated or overwritten with the exact same length of information. In cases like 946 * this, the file position needs to be reset 947 */ 948 position = 0; 949 reader.seek(position); // cannot be null here 950 951 // Now we can read new lines 952 position = readLines(reader); 953 last = tailable.lastModifiedFileTime(); 954 } 955 if (reOpen && reader != null) { 956 reader.close(); 957 } 958 ThreadUtils.sleep(delayDuration); 959 if (getRun() && reOpen) { 960 reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE); 961 reader.seek(position); 962 } 963 } 964 } catch (final InterruptedException e) { 965 Thread.currentThread().interrupt(); 966 listener.handle(e); 967 } catch (final Exception e) { 968 listener.handle(e); 969 } finally { 970 try { 971 IOUtils.close(reader); 972 } catch (final IOException e) { 973 listener.handle(e); 974 } 975 close(); 976 } 977 } 978 979 /** 980 * Requests the tailer to complete its current loop and return. 981 * 982 * @deprecated Use {@link #close()}. 983 */ 984 @Deprecated stop()985 public void stop() { 986 close(); 987 } 988 } 989