| FileWatcher.java |
/*
* $Id: FileWatcher.java,v 1.56 2013/01/04 10:11:15 agoubard Exp $
*
* See the COPYRIGHT file for redistribution and use restrictions.
*/
package org.xins.common.io;
import java.io.File;
import org.xins.common.Log;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.Utils;
/**
* File watcher thread.
*
* <p>This thread monitors one or more files, checking them at preset
* intervals. A listener is notified of the findings.
*
* <p>The check is performed every <em>n</em> seconds,
* where the interval <em>n</em> can be configured
* (see {@link #setInterval(int)} and {@link #getInterval()}).
*
* <p>Initially this thread will be a daemon thread. This can be changed by
* calling {@link #setDaemon(boolean)}.
*
* @version $Revision: 1.56 $ $Date: 2013/01/04 10:11:15 $
* @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
* @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
*
* @since XINS 1.0.0
*/
public class FileWatcher extends Thread {
/**
* Instance counter. Used to generate a unique ID for each instance.
*/
private static int INSTANCE_COUNT;
/**
* Lock object for <code>INSTANCE_COUNT</code>. Never <code>null</code>.
*/
private static final Object INSTANCE_COUNT_LOCK = new Object();
/**
* State in which this file watcher thread is not running.
*/
private static final int NOT_RUNNING = 1;
/**
* State in which this file watcher thread is currently running and has not
* been told to stop.
*/
private static final int RUNNING = 2;
/**
* State in which this file watcher thread is currently running, but has
* been told to stop.
*/
private static final int SHOULD_STOP = 3;
/**
* Fully-qualified name of this class.
*/
private final String _className;
/**
* Unique instance identifier.
*/
private final int _instanceID;
/**
* The files to watch. Not <code>null</code>.
*/
private File[] _files;
/**
* The string representation of the files to watch. Not <code>null</code>.
*/
protected String _filePaths;
/**
* Delay in seconds, at least 1. When the interval is uninitialized, the
* value of this field is less than 1.
*/
private int _interval;
/**
* The listener. Not <code>null</code>
*/
private final Listener _listener;
/**
* Timestamp of the last modification of the file. The value
* <code>-1L</code> indicates that the file could not be found the last
* time this was checked.
*
* <p>Initially this field is <code>-1L</code>.
*/
protected long _lastModified = -1L;
/**
* Current state. Never <code>null</code>. Value is one of the following
* values:
*
* <ul>
* <li>{@link #NOT_RUNNING}
* <li>{@link #RUNNING}
* <li>{@link #SHOULD_STOP}
* </ul>
*
* Once the thread is stopped, the state will be changed to
* {@link #NOT_RUNNING} again.
*/
private int _state;
/**
* Creates a new <code>FileWatcher</code> for the specified file.
*
* <p>The interval must be set before the thread can be started.
*
* @param file
* the name of the file to watch, cannot be <code>null</code>.
*
* @param listener
* the object to notify on events, cannot be <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>file == null || listener == null</code>
*
* @since XINS 1.2.0
*/
public FileWatcher(String file, Listener listener)
throws IllegalArgumentException {
this(file, 0, listener);
}
/**
* Creates a new <code>FileWatcher</code> for the specified file, with the
* specified interval.
*
* @param file
* the name of the file to watch, cannot be <code>null</code>.
*
* @param interval
* the interval in seconds, must be greater than or equal to 0.
* if the interval is 0 the interval must be set before the thread can
* be started.
*
* @param listener
* the object to notify on events, cannot be <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>file == null || listener == null || interval < 0</code>
*/
public FileWatcher(String file, int interval, Listener listener)
throws IllegalArgumentException {
this(new String[]{file}, interval, listener);
}
/**
* Creates a new <code>FileWatcher</code> for the specified set of files,
* with the specified interval.
*
* @param files
* the name of the files to watch, cannot be <code>null</code>.
* It should also have at least one file and none of the file should be <code>null</code>.
*
* @param interval
* the interval in seconds, must be greater than or equal to 0.
* if the interval is 0 the interval must be set before the thread can
* be started.
*
* @param listener
* the object to notify on events, cannot be <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>files == null || listener == null || interval < 0 || files.length < 1</code>
* or if one of the file is <code>null</code>.
*/
public FileWatcher(String[] files, int interval, Listener listener)
throws IllegalArgumentException {
// Check preconditions
MandatoryArgumentChecker.check("files", files, "listener", listener);
if (interval < 0) {
throw new IllegalArgumentException("interval (" + interval + ") < 0");
}
if (files.length < 1) {
throw new IllegalArgumentException("At least one file should be specified.");
}
for (int i = 0; i < files.length; i++) {
if (files[i] == null) {
throw new IllegalArgumentException("The file specified at index " + i + " is null.");
}
}
_className = getClass().getName();
// Determine the unique instance ID
int instanceID;
synchronized (INSTANCE_COUNT_LOCK) {
instanceID = INSTANCE_COUNT++;
}
// Initialize the fields
_instanceID = instanceID;
storeFiles(files);
_interval = interval;
_listener = listener;
_state = NOT_RUNNING;
// Configure thread as daemon
setDaemon(true);
// Set the name of this thread
configureThreadName();
// Immediately check if the file can be read from
firstCheck();
}
/**
* Stores the files in a class variable.
*
* @param files
* the String files to check, cannot be <code>null</code>.
*/
protected void storeFiles(String[] files) {
_files = new File[files.length];
_files[0] = new File(files[0]);
File baseDir = _files[0].getParentFile();
_filePaths = _files[0].getPath();
for (int i = 1; i < files.length; i++) {
_files[i] = new File(baseDir, files[i]);
_filePaths += ";" + _files[i].getPath();
}
}
/**
* Configures the name of this thread.
*/
private synchronized void configureThreadName() {
String name = _className + " #" + _instanceID + " [files=\"" +
_filePaths + "\"; interval=" + _interval + ']';
setName(name);
}
/**
* Performs the first check on the file to determine the date the file was
* last modified. This method is called from the constructors. If the file
* cannot be accessed due to a {@link SecurityException}, then this
* exception is logged and ignored.
*/
protected void firstCheck() {
for (int i = 0; i < _files.length; i++) {
File file = _files[i];
try {
if (file.canRead()) {
_lastModified = Math.max(_lastModified, file.lastModified());
}
// Ignore a SecurityException
} catch (SecurityException exception) {
Utils.logIgnoredException(exception);
}
}
}
/**
* Runs this thread. This method should not be called directly, call
* {@link #start()} instead. That method will call this method.
*
* @throws IllegalStateException
* if <code>{@link Thread#currentThread()} != this</code>, if the thread
* is already running or should stop, or if the interval was not set
* yet.
*/
public void run() throws IllegalStateException {
int interval;
int state;
synchronized (this) {
interval = _interval;
state = _state;
}
// Check preconditions
if (Thread.currentThread() != this) {
throw new IllegalStateException("Thread.currentThread() != this");
} else if (state == RUNNING) {
throw new IllegalStateException("Thread already running.");
} else if (state == SHOULD_STOP) {
throw new IllegalStateException("Thread should stop running.");
} else if (interval < 1) {
throw new IllegalStateException("Interval has not been set yet.");
}
Log.log_1200(_instanceID, _filePaths, interval);
// Move to the RUNNING state
synchronized (this) {
_state = RUNNING;
}
// Loop while we should keep running
boolean shouldStop = false;
while (! shouldStop) {
synchronized (this) {
try {
// Wait for the designated amount of time
wait(((long) interval) * 1000L);
} catch (InterruptedException exception) {
// The thread has been notified
}
// Should we stop?
shouldStop = (_state != RUNNING);
}
// If we do not have to stop yet, check if the file changed
if (! shouldStop) {
check();
}
}
// Thread stopped
Log.log_1203(_instanceID, _filePaths);
}
/**
* Returns the current interval.
*
* @return interval
* the current interval in seconds, always greater than or equal to 1,
* except if the interval is not initialized yet, in which case 0 is
* returned.
*/
public synchronized int getInterval() {
return _interval;
}
/**
* Changes the file check interval.
*
* @param newInterval
* the new interval in seconds, must be greater than or equal to 1.
*
* @throws IllegalArgumentException
* if <code>interval < 1</code>
*/
public synchronized void setInterval(int newInterval)
throws IllegalArgumentException {
// Check preconditions
if (newInterval < 1) {
throw new IllegalArgumentException(
"newInterval (" + newInterval + ") < 1");
}
// Change the interval
if (newInterval != _interval) {
Log.log_1201(_instanceID, _filePaths, _interval, newInterval);
_interval = newInterval;
}
// Update the thread name
configureThreadName();
}
/**
* Stops this thread.
*
* @throws IllegalStateException
* if the thread is currently not running or already stopping.
*/
public synchronized void end() throws IllegalStateException {
// Check state
if (_state == NOT_RUNNING) {
throw new IllegalStateException("Thread currently not running.");
} else if (_state == SHOULD_STOP) {
throw new IllegalStateException("Thread already stopping.");
}
Log.log_1202(_instanceID, _filePaths);
// Change the state and interrupt the thread
_state = SHOULD_STOP;
this.interrupt();
}
/**
* Checks if the file changed. The following algorithm is used:
*
* <ul>
* <li>check if the file is readable;
* <li>if so, then determine when the file was last modified;
* <li>if either the file existence check or the file modification check
* causes a {@link SecurityException} to be thrown, then
* {@link Listener#securityException(SecurityException)} is called
* and the method returns;
* <li>otherwise if the file is not readable (it may not exist), then
* {@link Listener#fileNotFound()} is called and the method returns;
* <li>otherwise if the file is readable, but previously was not,
* then {@link Listener#fileFound()} is called and the method
* returns;
* <li>otherwise if the file was modified, then
* {@link Listener#fileModified()} is called and the method returns;
* <li>otherwise the file was not modified, then
* {@link Listener#fileNotModified()} is called and the method
* returns.
* </ul>
*
* @since XINS 1.2.0
*/
public synchronized void check() {
// Variable to store the file modification timestamp in. The value -1
// indicates the file does not exist.
long lastModified = 0L;
// Check if the file can be read from and if so, when it was last
// modified
try {
lastModified = getLastModified();
// Authorisation problem; our code is not allowed to call canRead()
// and/or lastModified() on the File object
} catch (SecurityException securityException) {
// Notify the listener
try {
_listener.securityException(securityException);
// Ignore any exceptions thrown by the listener callback method
} catch (Throwable exception) {
Utils.logIgnoredException(exception);
}
// Short-circuit
return;
}
// A least one file can not be found
if (lastModified == -1L) {
// Set _lastModified to -1, which indicates the file did not exist
// last time it was checked.
_lastModified = -1L;
// Notify the listener
try {
_listener.fileNotFound();
// Ignore any exceptions thrown by the listener callback method
} catch (Throwable exception) {
Utils.logIgnoredException(exception);
}
// Previously a file could not be found, but now it can
} else if (_lastModified == -1L) {
// Update the field that stores the last known modification date
_lastModified = lastModified;
// Notify the listener
try {
_listener.fileFound();
// Ignore any exceptions thrown by the listener callback method
} catch (Throwable exception) {
Utils.logIgnoredException(exception);
}
// At least one file has been modified
} else if (lastModified != _lastModified) {
// Update the field that stores the last known modification date
_lastModified = lastModified;
// Notify listener
try {
_listener.fileModified();
// Ignore any exceptions thrown by the listener callback method
} catch (Throwable exception) {
Utils.logIgnoredException(exception);
}
// None of the files has not been modified
} else {
// Notify listener
try {
_listener.fileNotModified();
// Ignore any exceptions thrown by the listener callback method
} catch (Throwable exception) {
Utils.logIgnoredException(exception);
}
}
}
/**
* Gets the time at which the last file was modified.
* If for any reason, a file could no be read -1 is returned.
*
* @return
* the time of the last modified file or -1.
*
* @throws SecurityException
* if one of the file could not be read because of a security issue.
*/
protected long getLastModified() throws SecurityException {
long lastModified = 0L;
for (int i = 0; i < _files.length; i++) {
File file = _files[i];
if (file.canRead()) {
lastModified = Math.max(lastModified, file.lastModified());
} else {
return -1L;
}
}
return lastModified;
}
/**
* Interface for file watcher listeners.
*
* <p>Note that exceptions thrown by these callback methods will be ignored
* by the <code>FileWatcher</code>.
*
* @version $Revision: 1.56 $ $Date: 2013/01/04 10:11:15 $
* @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
*
* @since XINS 1.0.0
*/
public interface Listener {
/**
* Callback method, called if the file is checked but cannot be found.
* This method is called the first time the file is determined not to
* exist, but also each consecutive time the file is still determined
* not to be found.
*/
void fileNotFound();
/**
* Callback method, called if the file is found for the first time since
* the <code>FileWatcher</code> was started or after it has been deleted.
* Each consecutive time the file still exists (and is readable), either
* {@link #fileModified()} or {@link #fileNotModified()} is called.
*/
void fileFound();
/**
* Callback method, called if an authorisation error prevents that the
* file is checked for existence and last modification date.
*
* @param exception
* the caught exception, not <code>null</code>.
*/
void securityException(SecurityException exception);
/**
* Callback method, called if the file was checked and found to be
* modified.
*/
void fileModified();
/**
* Callback method, called if the file was checked but found not to be
* modified.
*/
void fileNotModified();
}
}