/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.io.input;

import static org.apache.commons.io.IOUtils.EOF;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;

import org.apache.commons.io.FileUtils;

Simple implementation of the unix "tail -f" functionality.

1. Create a TailerListener implementation

First you need to create a TailerListener implementation (TailerListenerAdapter is provided for convenience so that you don't have to implement every method).

For example:

 public class MyTailerListener extends TailerListenerAdapter {
     public void handle(String line) {
         System.out.println(line);
     }
 }

2. Using a Tailer

You can create and use a Tailer in one of three ways:

An example of each of these is shown below.

2.1 Using the static helper method

     TailerListener listener = new MyTailerListener();
     Tailer tailer = Tailer.create(file, listener, delay);

2.2 Using an Executor

     TailerListener listener = new MyTailerListener();
     Tailer tailer = new Tailer(file, listener, delay);
     // stupid executor impl. for demo purposes
     Executor executor = new Executor() {
         public void execute(Runnable command) {
             command.run();
          }
     };
     executor.execute(tailer);

2.3 Using a Thread

     TailerListener listener = new MyTailerListener();
     Tailer tailer = new Tailer(file, listener, delay);
     Thread thread = new Thread(tailer);
     thread.setDaemon(true); // optional
     thread.start();

3. Stopping a Tailer

Remember to stop the tailer when you have done with it:

     tailer.stop();

4. Interrupting a Tailer

You can interrupt the thread a tailer is running on by calling Thread.interrupt().

     thread.interrupt();

If you interrupt a tailer, the tailer listener is called with the InterruptedException.

The file is read using the default charset; this can be overridden if necessary

See Also:
Version:$Id$
Since:2.0
Since:2.5 Updated behavior and documentation for Thread.interrupt()
/** * Simple implementation of the unix "tail -f" functionality. * * <h2>1. Create a TailerListener implementation</h2> * <p> * First you need to create a {@link TailerListener} implementation * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to * implement every method). * </p> * * <p>For example:</p> * <pre> * public class MyTailerListener extends TailerListenerAdapter { * public void handle(String line) { * System.out.println(line); * } * }</pre> * * <h2>2. Using a Tailer</h2> * * <p> * You can create and use a Tailer in one of three ways: * </p> * <ul> * <li>Using one of the static helper methods: * <ul> * <li>{@link Tailer#create(File, TailerListener)}</li> * <li>{@link Tailer#create(File, TailerListener, long)}</li> * <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li> * </ul> * </li> * <li>Using an {@link java.util.concurrent.Executor}</li> * <li>Using an {@link Thread}</li> * </ul> * * <p> * An example of each of these is shown below. * </p> * * <h3>2.1 Using the static helper method</h3> * * <pre> * TailerListener listener = new MyTailerListener(); * Tailer tailer = Tailer.create(file, listener, delay);</pre> * * <h3>2.2 Using an Executor</h3> * * <pre> * TailerListener listener = new MyTailerListener(); * Tailer tailer = new Tailer(file, listener, delay); * * // stupid executor impl. for demo purposes * Executor executor = new Executor() { * public void execute(Runnable command) { * command.run(); * } * }; * * executor.execute(tailer); * </pre> * * * <h3>2.3 Using a Thread</h3> * <pre> * TailerListener listener = new MyTailerListener(); * Tailer tailer = new Tailer(file, listener, delay); * Thread thread = new Thread(tailer); * thread.setDaemon(true); // optional * thread.start();</pre> * * <h2>3. Stopping a Tailer</h2> * <p>Remember to stop the tailer when you have done with it:</p> * <pre> * tailer.stop(); * </pre> * * <h2>4. Interrupting a Tailer</h2> * <p>You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}.</p> * <pre> * thread.interrupt(); * </pre> * <p>If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}.</p> * * <p>The file is read using the default charset; this can be overridden if necessary</p> * @see TailerListener * @see TailerListenerAdapter * @version $Id$ * @since 2.0 * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()} */
public class Tailer implements Runnable { private static final int DEFAULT_DELAY_MILLIS = 1000; private static final String RAF_MODE = "r"; private static final int DEFAULT_BUFSIZE = 4096; // The default charset used for reading files private static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
Buffer on top of RandomAccessFile.
/** * Buffer on top of RandomAccessFile. */
private final byte inbuf[];
The file which will be tailed.
/** * The file which will be tailed. */
private final File file;
The character set that will be used to read the file.
/** * The character set that will be used to read the file. */
private final Charset cset;
The amount of time to wait for the file to be updated.
/** * The amount of time to wait for the file to be updated. */
private final long delayMillis;
Whether to tail from the end or start of file
/** * Whether to tail from the end or start of file */
private final boolean end;
The listener to notify of events when tailing.
/** * The listener to notify of events when tailing. */
private final TailerListener listener;
Whether to close and reopen the file whilst waiting for more input.
/** * Whether to close and reopen the file whilst waiting for more input. */
private final boolean reOpen;
The tailer will run as long as this value is true.
/** * The tailer will run as long as this value is true. */
private volatile boolean run = true;
Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
Params:
  • file – The file to follow.
  • listener – the TailerListener to use.
/** * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s. * @param file The file to follow. * @param listener the TailerListener to use. */
public Tailer(final File file, final TailerListener listener) { this(file, listener, DEFAULT_DELAY_MILLIS); }
Creates a Tailer for the given file, starting from the beginning.
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
/** * Creates a Tailer for the given file, starting from the beginning. * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. */
public Tailer(final File file, final TailerListener listener, final long delayMillis) { this(file, listener, delayMillis, false); }
Creates a Tailer for the given file, with a delay other than the default 1.0s.
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
  • end – Set to true to tail from the end of the file, false to tail from the beginning of the file.
/** * Creates a Tailer for the given file, with a delay other than the default 1.0s. * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. */
public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) { this(file, listener, delayMillis, end, DEFAULT_BUFSIZE); }
Creates a Tailer for the given file, with a delay other than the default 1.0s.
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
  • end – Set to true to tail from the end of the file, false to tail from the beginning of the file.
  • reOpen – if true, close and reopen the file between reading chunks
/** * Creates a Tailer for the given file, with a delay other than the default 1.0s. * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen if true, close and reopen the file between reading chunks */
public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) { this(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE); }
Creates a Tailer for the given file, with a specified buffer size.
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
  • end – Set to true to tail from the end of the file, false to tail from the beginning of the file.
  • bufSize – Buffer size
/** * Creates a Tailer for the given file, with a specified buffer size. * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param bufSize Buffer size */
public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufSize) { this(file, listener, delayMillis, end, false, bufSize); }
Creates a Tailer for the given file, with a specified buffer size.
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
  • end – Set to true to tail from the end of the file, false to tail from the beginning of the file.
  • reOpen – if true, close and reopen the file between reading chunks
  • bufSize – Buffer size
/** * Creates a Tailer for the given file, with a specified buffer size. * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen if true, close and reopen the file between reading chunks * @param bufSize Buffer size */
public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufSize) { this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize); }
Creates a Tailer for the given file, with a specified buffer size.
Params:
  • file – the file to follow.
  • cset – the Charset to be used for reading the file
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
  • end – Set to true to tail from the end of the file, false to tail from the beginning of the file.
  • reOpen – if true, close and reopen the file between reading chunks
  • bufSize – Buffer size
/** * Creates a Tailer for the given file, with a specified buffer size. * @param file the file to follow. * @param cset the Charset to be used for reading the file * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen if true, close and reopen the file between reading chunks * @param bufSize Buffer size */
public Tailer(final File file, final Charset cset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen , final int bufSize) { this.file = file; this.delayMillis = delayMillis; this.end = end; this.inbuf = new byte[bufSize]; // Save and prepare the listener this.listener = listener; listener.init(this); this.reOpen = reOpen; this.cset = cset; }
Creates and starts a Tailer for the given file.
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
  • end – Set to true to tail from the end of the file, false to tail from the beginning of the file.
  • bufSize – buffer size.
Returns:The new tailer
/** * Creates and starts a Tailer for the given file. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param bufSize buffer size. * @return The new tailer */
public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufSize) { return create(file, listener, delayMillis, end, false, bufSize); }
Creates and starts a Tailer for the given file.
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
  • end – Set to true to tail from the end of the file, false to tail from the beginning of the file.
  • reOpen – whether to close/reopen the file between chunks
  • bufSize – buffer size.
Returns:The new tailer
/** * Creates and starts a Tailer for the given file. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen whether to close/reopen the file between chunks * @param bufSize buffer size. * @return The new tailer */
public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufSize) { return create(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize); }
Creates and starts a Tailer for the given file.
Params:
  • file – the file to follow.
  • charset – the character set to use for reading the file
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
  • end – Set to true to tail from the end of the file, false to tail from the beginning of the file.
  • reOpen – whether to close/reopen the file between chunks
  • bufSize – buffer size.
Returns:The new tailer
/** * Creates and starts a Tailer for the given file. * * @param file the file to follow. * @param charset the character set to use for reading the file * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen whether to close/reopen the file between chunks * @param bufSize buffer size. * @return The new tailer */
public static Tailer create(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen ,final int bufSize) { final Tailer tailer = new Tailer(file, charset, listener, delayMillis, end, reOpen, bufSize); final Thread thread = new Thread(tailer); thread.setDaemon(true); thread.start(); return tailer; }
Creates and starts a Tailer for the given file with default buffer size.
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
  • end – Set to true to tail from the end of the file, false to tail from the beginning of the file.
Returns:The new tailer
/** * Creates and starts a Tailer for the given file with default buffer size. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @return The new tailer */
public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end) { return create(file, listener, delayMillis, end, DEFAULT_BUFSIZE); }
Creates and starts a Tailer for the given file with default buffer size.
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
  • end – Set to true to tail from the end of the file, false to tail from the beginning of the file.
  • reOpen – whether to close/reopen the file between chunks
Returns:The new tailer
/** * Creates and starts a Tailer for the given file with default buffer size. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen whether to close/reopen the file between chunks * @return The new tailer */
public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) { return create(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE); }
Creates and starts a Tailer for the given file, starting at the beginning of the file
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
  • delayMillis – the delay between checks of the file for new content in milliseconds.
Returns:The new tailer
/** * Creates and starts a Tailer for the given file, starting at the beginning of the file * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @return The new tailer */
public static Tailer create(final File file, final TailerListener listener, final long delayMillis) { return create(file, listener, delayMillis, false); }
Creates and starts a Tailer for the given file, starting at the beginning of the file with the default delay of 1.0s
Params:
  • file – the file to follow.
  • listener – the TailerListener to use.
Returns:The new tailer
/** * Creates and starts a Tailer for the given file, starting at the beginning of the file * with the default delay of 1.0s * * @param file the file to follow. * @param listener the TailerListener to use. * @return The new tailer */
public static Tailer create(final File file, final TailerListener listener) { return create(file, listener, DEFAULT_DELAY_MILLIS, false); }
Return the file.
Returns:the file
/** * Return the file. * * @return the file */
public File getFile() { return file; }
Gets whether to keep on running.
Returns:whether to keep on running.
Since:2.5
/** * Gets whether to keep on running. * * @return whether to keep on running. * @since 2.5 */
protected boolean getRun() { return run; }
Return the delay in milliseconds.
Returns:the delay in milliseconds.
/** * Return the delay in milliseconds. * * @return the delay in milliseconds. */
public long getDelay() { return delayMillis; }
Follows changes in the file, calling the TailerListener's handle method for each new line.
/** * Follows changes in the file, calling the TailerListener's handle method for each new line. */
@Override public void run() { RandomAccessFile reader = null; try { long last = 0; // The last time the file was checked for changes long position = 0; // position within the file // Open the file while (getRun() && reader == null) { try { reader = new RandomAccessFile(file, RAF_MODE); } catch (final FileNotFoundException e) { listener.fileNotFound(); } if (reader == null) { Thread.sleep(delayMillis); } else { // The current position in the file position = end ? file.length() : 0; last = file.lastModified(); reader.seek(position); } } while (getRun()) { final boolean newer = FileUtils.isFileNewer(file, last); // IO-279, must be done first // Check the file length to see if it was rotated final long length = file.length(); if (length < position) { // File was rotated listener.fileRotated(); // Reopen the reader after rotation ensuring that the old file is closed iff we re-open it // successfully try (RandomAccessFile save = reader) { reader = new RandomAccessFile(file, RAF_MODE); // At this point, we're sure that the old file is rotated // Finish scanning the old file and then we'll start with the new one try { readLines(save); } catch (final IOException ioe) { listener.handle(ioe); } position = 0; } catch (final FileNotFoundException e) { // in this case we continue to use the previous reader and position values listener.fileNotFound(); Thread.sleep(delayMillis); } continue; } else { // File was not rotated // See if the file needs to be read again if (length > position) { // The file has more content than it did last time position = readLines(reader); last = file.lastModified(); } else if (newer) { /* * This can happen if the file is truncated or overwritten with the exact same length of * information. In cases like this, the file position needs to be reset */ position = 0; reader.seek(position); // cannot be null here // Now we can read new lines position = readLines(reader); last = file.lastModified(); } } if (reOpen && reader != null) { reader.close(); } Thread.sleep(delayMillis); if (getRun() && reOpen) { reader = new RandomAccessFile(file, RAF_MODE); reader.seek(position); } } } catch (final InterruptedException e) { Thread.currentThread().interrupt(); listener.handle(e); } catch (final Exception e) { listener.handle(e); } finally { try { if (reader != null) { reader.close(); } } catch (final IOException e) { listener.handle(e); } stop(); } }
Allows the tailer to complete its current loop and return.
/** * Allows the tailer to complete its current loop and return. */
public void stop() { this.run = false; }
Read new lines.
Params:
  • reader – The file to read
Throws:
Returns:The new position after the lines have been read
/** * Read new lines. * * @param reader The file to read * @return The new position after the lines have been read * @throws java.io.IOException if an I/O error occurs. */
private long readLines(final RandomAccessFile reader) throws IOException { try (ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64)) { long pos = reader.getFilePointer(); long rePos = pos; // position to re-read int num; boolean seenCR = false; while (getRun() && ((num = reader.read(inbuf)) != EOF)) { for (int i = 0; i < num; i++) { final byte ch = inbuf[i]; switch ( ch ) { case '\n': seenCR = false; // swallow CR before LF listener.handle(new String(lineBuf.toByteArray(), cset)); lineBuf.reset(); rePos = pos + i + 1; break; case '\r': if (seenCR) { lineBuf.write('\r'); } seenCR = true; break; default: if (seenCR) { seenCR = false; // swallow final CR listener.handle(new String(lineBuf.toByteArray(), cset)); lineBuf.reset(); rePos = pos + i + 1; } lineBuf.write(ch); } } pos = reader.getFilePointer(); } reader.seek(rePos); // Ensure we can re-read if necessary if (listener instanceof TailerListenerAdapter) { ((TailerListenerAdapter) listener).endOfFileReached(); } return rePos; } } }