• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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