/*
* $Id$
*
* Copyright (c) 2010 by Joel Uckelman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.tools.io;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import VASSAL.tools.concurrent.listener.DefaultEventListenerSupport;
import VASSAL.tools.concurrent.listener.EventListener;
import VASSAL.tools.concurrent.listener.EventListenerSupport;
/**
* Tail a file. This class is designed to behave similarly to the UNIX
* command <code>tail -f</code>, watching the file for changes and
* reporting them to listeners. The tailer may be stopped when it is not
* needed; if it is restarted, it will remember its last position and
* resume reading where it left off.
*
* @author Joel Uckelman
* @since 3.2.0
*/
public class Tailer {
private static final Logger logger = LoggerFactory.getLogger(Tailer.class);
protected static final long DEFAULT_POLL_INTERVAL = 1000L;
protected final File file;
protected final long poll_interval;
protected long position = 0L;
protected volatile boolean tailing = false;
protected final EventListenerSupport<String> lsup;
/**
* Creates a file tailer with the default polling interval.
*
* @param file the file to tail
*/
public Tailer(File file) {
this(file, DEFAULT_POLL_INTERVAL);
}
/**
* Creates a file tailer.
*
* @param file the file to tail
* @param poll_interval the polling interval, in milliseconds
*/
public Tailer(File file, long poll_interval) {
if (file == null) throw new IllegalArgumentException("file == null");
this.file = file;
this.poll_interval = poll_interval;
this.lsup = new DefaultEventListenerSupport<String>(this);
}
/**
* Creates a file tailer.
*
* @param file the file to tail
* @param poll_interval the polling interval, in milliseconds
* @param lsup the listener support
*/
public Tailer(File file, long poll_interval,
EventListenerSupport<String> lsup) {
if (file == null) throw new IllegalArgumentException("file == null");
if (lsup == null) throw new IllegalArgumentException("lsup == null");
this.file = file;
this.poll_interval = poll_interval;
this.lsup = lsup;
}
/**
* Starts tailing the file.
*/
public synchronized void start() throws IOException {
// NB: This method is synchronized to ensure that there is never more
// than one tailer thread at at time.
if (!tailing) {
if (!file.exists()) {
throw new IOException(file.getAbsolutePath() + " does not exist");
}
if (file.isDirectory()) {
throw new IOException(file.getAbsolutePath() + " is a directory");
}
tailing = true;
new Thread(new Monitor(), "tailing " + file.getAbsolutePath()).start();
}
}
/**
* Stops tailing the file.
*/
public void stop() {
tailing = false;
}
/**
* Checks whether the tailer is running.
*
* @return <code>true</code> if the tailer is running
*/
public boolean isTailing() {
return tailing;
}
/**
* Gets the file being tailed.
*
* @return the file
*/
public File getFile() {
return file;
}
/**
* Adds an {@link EventListener}.
*
* @param l the listener to add
*/
public void addEventListener(EventListener<? super String> l) {
lsup.addEventListener(l);
}
/**
* Removes an {@link EventListener}.
*
* @param l the listener to remove
*/
public void removeEventListener(EventListener<? super String> l) {
lsup.removeEventListener(l);
}
/**
* Checks whether there are any {@link EventListener}s.
*
* @return <code>true</code> if there are any listeners
*/
public boolean hasEventListeners() {
return lsup.hasEventListeners();
}
/**
* Gets the list of listerners.
*
* @return the list of listeners
*/
public List<EventListener<? super String>> getEventListeners() {
return lsup.getEventListeners();
}
private class Monitor implements Runnable {
public void run() {
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(file, "r");
// read until we're told to stop
while (tailing) {
final long length = raf.length();
if (length < position) {
// file has been truncated, reopen it
raf = new RandomAccessFile(file, "r");
position = 0L;
}
else if (length > position) {
// new lines have been written, read them
raf.seek(position);
String line;
while ((line = raf.readLine()) != null) {
// readLine strips newlines, we put them back
lsup.notify(line + "\n");
}
position = raf.getFilePointer();
}
// we have reached EOF, sleep
Thread.sleep(poll_interval);
}
}
catch (IOException e) {
// FIXME: there should be an error listener; we can't handle exceptions here
logger.error("", e);
}
catch (InterruptedException e) {
logger.error("", e);
}
finally {
IOUtils.closeQuietly(raf);
}
}
}
public static void main(String args[]) throws IOException {
final Tailer t = new Tailer(new File(args[0]));
t.addEventListener(new EventListener<String>() {
public void receive(Object src, String s) {
System.out.print(s);
}
});
t.start();
}
}