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(); } }