From ffca820860d90efc204b1b4f37762f39473b66e4 Mon Sep 17 00:00:00 2001 From: Doug Pedrick Date: Wed, 19 Dec 2012 20:22:14 -0600 Subject: [PATCH] Initial file watcher code. --- .../openrocket/startup/ApplicationModule.java | 13 +- .../sf/openrocket/util/watcher/Directory.java | 87 +++++++ .../util/watcher/DirectoryChangeReactor.java | 26 ++ .../watcher/DirectoryChangeReactorImpl.java | 229 +++++++++++++++++ .../util/watcher/DirectoryMonitor.java | 231 ++++++++++++++++++ .../openrocket/util/watcher/WatchEvent.java | 49 ++++ .../util/watcher/WatchEventKind.java | 75 ++++++ .../openrocket/util/watcher/WatchService.java | 160 ++++++++++++ .../util/watcher/WatchedEventHandler.java | 33 +++ .../openrocket/util/watcher/WatchedFile.java | 148 +++++++++++ .../DirectoryChangeReactorImplTest.java | 162 ++++++++++++ .../util/watcher/DirectoryMonitorTest.java | 83 +++++++ .../util/watcher/DirectoryTest.java | 177 ++++++++++++++ .../util/watcher/WatchedFileTest.java | 82 +++++++ 14 files changed, 1550 insertions(+), 5 deletions(-) create mode 100644 core/src/net/sf/openrocket/util/watcher/Directory.java create mode 100644 core/src/net/sf/openrocket/util/watcher/DirectoryChangeReactor.java create mode 100644 core/src/net/sf/openrocket/util/watcher/DirectoryChangeReactorImpl.java create mode 100644 core/src/net/sf/openrocket/util/watcher/DirectoryMonitor.java create mode 100644 core/src/net/sf/openrocket/util/watcher/WatchEvent.java create mode 100644 core/src/net/sf/openrocket/util/watcher/WatchEventKind.java create mode 100644 core/src/net/sf/openrocket/util/watcher/WatchService.java create mode 100644 core/src/net/sf/openrocket/util/watcher/WatchedEventHandler.java create mode 100644 core/src/net/sf/openrocket/util/watcher/WatchedFile.java create mode 100644 core/test/net/sf/openrocket/util/watcher/DirectoryChangeReactorImplTest.java create mode 100644 core/test/net/sf/openrocket/util/watcher/DirectoryMonitorTest.java create mode 100644 core/test/net/sf/openrocket/util/watcher/DirectoryTest.java create mode 100644 core/test/net/sf/openrocket/util/watcher/WatchedFileTest.java diff --git a/core/src/net/sf/openrocket/startup/ApplicationModule.java b/core/src/net/sf/openrocket/startup/ApplicationModule.java index 32d35290f..6b775a94a 100644 --- a/core/src/net/sf/openrocket/startup/ApplicationModule.java +++ b/core/src/net/sf/openrocket/startup/ApplicationModule.java @@ -1,17 +1,20 @@ package net.sf.openrocket.startup; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; - import com.google.inject.AbstractModule; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.util.watcher.DirectoryChangeReactor; +import net.sf.openrocket.util.watcher.DirectoryChangeReactorImpl; + public class ApplicationModule extends AbstractModule { - + @Override protected void configure() { bind(LogHelper.class).toInstance(Application.getLogger()); bind(Preferences.class).toInstance(Application.getPreferences()); bind(Translator.class).toInstance(Application.getTranslator()); + bind(DirectoryChangeReactor.class).to(DirectoryChangeReactorImpl.class); } - + } diff --git a/core/src/net/sf/openrocket/util/watcher/Directory.java b/core/src/net/sf/openrocket/util/watcher/Directory.java new file mode 100644 index 000000000..3d1269abb --- /dev/null +++ b/core/src/net/sf/openrocket/util/watcher/Directory.java @@ -0,0 +1,87 @@ +package net.sf.openrocket.util.watcher; + +import java.io.File; +import java.util.HashMap; + +/** + * A kind of watched file that is a directory. + */ +public class Directory extends WatchedFile { + + /** + * The contents. + */ + private final HashMap contents = new HashMap(); + + /** + * Internal lock object. + */ + private final Object lock = new Object(); + + /** + * Construct a directory to be watched. + * + * @param dir the directory to be watched + * + * @throws IllegalArgumentException if dir is null, does not exist, or is not a directory + */ + public Directory(File dir) throws IllegalArgumentException { + super(dir); + if (dir == null || !dir.isDirectory() || !dir.exists()) { + throw new IllegalArgumentException("Invalid directory."); + } + + init(); + } + + /** + * Initialize the directory handling. + */ + private void init() { + String[] result = list(); + for (String s : result) { + File t = new File(getTarget(), s); + if (t.exists()) { + contents.put(s, new WatchedFile(t)); + } + } + } + + /** + * Get the size of the directory's immediate contents (the number of files or subdirectories). + * + * @return the number of files and directories in this directory (not deep) + */ + public int size() { + return list().length; + } + + /** + * Get the list of filenames within this directory. + * + * @return an array of filenames + */ + String[] list() { + synchronized (lock) { + return getTarget().list(); + } + } + + /** + * Get the watched file contents. + * + * @return a map of watched files + */ + protected final HashMap getContents() { + return contents; + } + + /** + * Shared lock. + * + * @return a lock + */ + protected final Object getLock() { + return lock; + } +} diff --git a/core/src/net/sf/openrocket/util/watcher/DirectoryChangeReactor.java b/core/src/net/sf/openrocket/util/watcher/DirectoryChangeReactor.java new file mode 100644 index 000000000..75a66e9c1 --- /dev/null +++ b/core/src/net/sf/openrocket/util/watcher/DirectoryChangeReactor.java @@ -0,0 +1,26 @@ +package net.sf.openrocket.util.watcher; + +/** + * This interface abstracts the public API for a directory change reactor. In order to use the watcher subsystem, clients may use the default + * change reactor (that implements this interface), or use the WatchService directly. This interface is more of a convenience abstraction. + *

+ * This only monitors directories. If you want to monitor an individual file, it is recommended that you monitor the directory that the file resides + * within, then filter the WatchEvents accordingly. + */ +public interface DirectoryChangeReactor { + + /** + * Register an event handler with the reactor. The event handler will be called when either a creation, modification, or deletion event is detected + * in the watched directory. Or a modification or a deletion event detected upon a watched file. + * + * @param theEventHandler the handler to be called when an event is detected + */ + void registerHandler(WatchedEventHandler theEventHandler); + + /** + * Unregister an event handler with the reactor. + * + * @param theEventHandler the handler + */ + void unregisterHandler(WatchedEventHandler theEventHandler); +} diff --git a/core/src/net/sf/openrocket/util/watcher/DirectoryChangeReactorImpl.java b/core/src/net/sf/openrocket/util/watcher/DirectoryChangeReactorImpl.java new file mode 100644 index 000000000..c6736d05a --- /dev/null +++ b/core/src/net/sf/openrocket/util/watcher/DirectoryChangeReactorImpl.java @@ -0,0 +1,229 @@ +package net.sf.openrocket.util.watcher; + +import com.google.inject.Inject; + +import net.sf.openrocket.logging.LogHelper; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * This class is responsible for monitoring changes to files and directories and dispatching handle events appropriately. In order to use the watcher + * subsystem, clients may use the default change reactor (this class), or use the WatchService directly. This class is more of a convenience + * abstraction and doesn't necessarily represent the most efficient mechanism in all situations, but is sufficient for general purpose file watching + * (short of using JDK 7). + *

+ * This reactor creates a new WatchService for each handler. A handler owns the business logic to be performed whenever a state change is detected in a + * monitored directory. Each handler can monitor one directory (and optionally that directory's subdirectories). It's possible to implement a + * different reactor that allows one handler to monitor many different directories or files. + *

+ * The default polling interval is 5 seconds. To override the polling interval a system property (openrocket.watcher.poll) can be specified. The time + * value must be specified in milliseconds. + *

+ * For example, to change the interval to 10 seconds: -Dopenrocket.watcher.poll=10000 + *

+ *

+ * Example Usage of this class: + *

+ * 
+ *
+ * class MyHandler extends WatchedEventHandler  {
+ *
+ *   private Directory dirToWatch = new Directory(new File("/tmp"));
+ *
+ *   public Directory watchTarget() {
+ *     return dirToWatch;
+ *   }
+ *
+ *   public boolean watchRecursively() {
+ *     return true;
+ *   }
+ *
+ *   public void handleEvents(List> theEvents) {
+ *     for (int i = 0; i < events.size(); i++) {
+ *       WatchEvent watchEvent = events.get(i);
+ *       // process the event
+ *     }
+ *   }
+ * }
+ * 
+ *
+ * Guice (assuming that the binding is performed in an appropriate module):
+ * {@literal @}Inject
+ *  DirectoryChangeReactor reactor;
+ *  reactor.registerHandler(new MyHandler());
+ *
+ * Programmatically:
+ *  DirectoryChangeReactor reactor = new DirectoryChangeReactorImpl();
+ *  reactor.registerHandler(new MyHandler());
+ * 
+ */ +public class DirectoryChangeReactorImpl implements DirectoryChangeReactor { + + /** + * Property for polling frequency. + */ + public static final String WATCHER_POLLING_INTERVAL_PROPERTY = "openrocket.watcher.poll"; + /** + * The polling delay. Defaults to 5 seconds. + */ + public static final long DEFAULT_POLLING_DELAY = 5000; + /** + * The polling delay. Defaults to 5 seconds. + */ + private static long pollingDelay = DEFAULT_POLLING_DELAY; + + @Inject + private LogHelper log; + + /** + * Scheduler. + */ + private ScheduledExecutorService scheduler; + /** + * The ScheduledFuture. + */ + private ScheduledFuture directoryPollerFuture; + /** + * The runnable that does most of the work. + */ + private final DirectoryPoller directoryPoller = new DirectoryPoller(); + /** + * The collection of registered handlers. A new watch service is created for each handler. + */ + private final Map activeWatchers = new ConcurrentHashMap(); + /** + * Synchronization object. + */ + private final Object lock = new Object(); + + static { + try { + if (System.getProperty(WATCHER_POLLING_INTERVAL_PROPERTY) != null) { + pollingDelay = Long.parseLong(System.getProperty(WATCHER_POLLING_INTERVAL_PROPERTY)); + } + } + catch (Exception e) { + pollingDelay = DEFAULT_POLLING_DELAY; + } + } + + /** + * Constructor. + */ + public DirectoryChangeReactorImpl() { + this(LogHelper.getInstance()); + } + + /** + * Injected constructor. + */ + @Inject + public DirectoryChangeReactorImpl(LogHelper theLogger) { + log = theLogger; + startup(); + } + + /** + * Idempotent initialization. + */ + private void startup() { + + synchronized (lock) { + if (scheduler != null) { + scheduler.shutdownNow(); + } + scheduler = Executors.newScheduledThreadPool(1); + + scheduleFuture(); + } + } + + /** + * Cause all watch services to close. This method is idempotent. + */ + public void shutdown() { + synchronized (lock) { + + if (scheduler != null) { + scheduler.shutdownNow(); + } + for (Iterator iterator = activeWatchers.keySet().iterator(); iterator.hasNext(); ) { + WatchedEventHandler next = iterator.next(); + activeWatchers.get(next).close(); + iterator.remove(); + } + } + } + + @Override + public void registerHandler(WatchedEventHandler theEventHandler) { + synchronized (lock) { + unregisterHandler(theEventHandler); + final WatchService watchService = new WatchService(theEventHandler.watchRecursively()); + watchService.register(theEventHandler.watchTarget()); + activeWatchers.put(theEventHandler, watchService); + } + + } + + @Override + public void unregisterHandler(WatchedEventHandler theEventHandler) { + synchronized (lock) { + if (activeWatchers.containsKey(theEventHandler)) { + activeWatchers.get(theEventHandler).close(); + activeWatchers.remove(theEventHandler); + } + } + } + + /** + * Schedule the periodic poll. + */ + private void scheduleFuture() { + if (directoryPollerFuture != null + && !directoryPollerFuture.isDone() + && !directoryPollerFuture.isCancelled()) { + directoryPollerFuture.cancel(false); + } + directoryPollerFuture = scheduler.schedule(directoryPoller, pollingDelay, TimeUnit.MILLISECONDS); + } + + /** + * This runnable is responsible for checking all watch services for new events. + */ + private final class DirectoryPoller implements Runnable { + @Override + public void run() { + try { + if (!activeWatchers.isEmpty()) { + for (Iterator iterator = activeWatchers.keySet().iterator(); iterator.hasNext(); ) { + WatchedEventHandler next = iterator.next(); + try { + WatchService watchService = activeWatchers.get(next); + Collection watchKeyCollection = watchService.poll(); + if (!watchKeyCollection.isEmpty()) { + for (WatchService.WatchKey watchKey : watchKeyCollection) { + next.handleEvents(watchKey.pollEvents()); + } + } + } + catch (Exception e) { + log.error("Error notifying handler of watch event. Removing registration.", e); + iterator.remove(); + } + } + } + } + finally { + scheduleFuture(); + } + } + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/util/watcher/DirectoryMonitor.java b/core/src/net/sf/openrocket/util/watcher/DirectoryMonitor.java new file mode 100644 index 000000000..681f1ae15 --- /dev/null +++ b/core/src/net/sf/openrocket/util/watcher/DirectoryMonitor.java @@ -0,0 +1,231 @@ +package net.sf.openrocket.util.watcher; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This class checks for changes in directories. Supported events: file creation, subdirectory creation, file modification, file and subdirectory + * deletion. + *

+ * Synchronize calls to this class externally to ensure thread safety. + */ +final class DirectoryMonitor { + + /** + * The directories being monitored. + */ + private Set monitored = new HashSet(); + + /** + * Flag that indicates if subdirectories should automatically be monitored when they are created. + */ + private boolean monitorSubdirectories = false; + + /** + * Constructor. Self registration is set to false. + */ + DirectoryMonitor() { + this(false); + } + + /** + * Constructor. + * + * @param monitorOnCreate if true auto-monitor new subdirectories + */ + DirectoryMonitor(boolean monitorOnCreate) { + monitorSubdirectories = monitorOnCreate; + } + + /** + * Register a directory to be monitored for changes. + * + * @param dir directory to monitor + */ + void register(final Directory dir) { + if (dir != null && !monitored.contains(dir)) { + monitored.add(dir); + if (monitorSubdirectories) { + recurse(dir); + } + } + } + + private void recurse(final Directory dir) { + synchronized (dir.getLock()) { + String[] list = dir.list(); + for (String file : list) { + final File f = new File(dir.getTarget(), file); + if (f.isDirectory()) { + register(new Directory(f)); + } + } + } + } + + /** + * Clear/close and resources being monitored. + */ + void close() { + monitored.clear(); + } + + /** + * Unregister a directory. + * + * @param dir + */ + private void unregister(Directory dir) { + if (dir != null) { + monitored.remove(dir); + } + } + + /** + * + * The main business logic of this directory monitor. + * + * @return a WatchEvent instance or null + */ + Collection check() { + Map result = new HashMap(); + + for (Directory directory : monitored) { + synchronized (directory.getLock()) { + if (directory.exists()) { + Map watchedFiles = (Map) directory.getContents().clone(); + String[] filesCurrentlyInDirectory = directory.list(); + for (String s : filesCurrentlyInDirectory) { + if (directory.getContents().containsKey(s)) { + WatchEvent we = directory.getContents().get(s).check(); + if (!we.equals(WatchEvent.NO_EVENT)) { + add(result, directory, we); + } + } + else { + final File f = new File(directory.getTarget(), s); + WatchedFile nf = new WatchedFile(f); + add(result, directory, nf.createEvent()); + directory.getContents().put(s, nf); + if (f.isDirectory() && monitorSubdirectories) { + register(new Directory(f)); + } + } + watchedFiles.remove(s); + } + + for (String file : watchedFiles.keySet()) { + WatchEvent we = watchedFiles.get(file).check(); + if (!we.equals(WatchEvent.NO_EVENT)) { + add(result, directory, we); + if (we.kind().equals(WatchEventKind.ENTRY_DELETE)) { + directory.getContents().remove(file); + } + } + } + } + else { + add(result, directory, new DirectoryWatchEvent(WatchEventKind.ENTRY_DELETE, directory.getTarget())); + unregister(directory); + } + } + } + return result.values(); + } + + private void add(Map keyList, Directory dir, WatchEvent we) { + WatchKeyImpl key = keyList.get(dir); + if (key == null) { + key = new WatchKeyImpl(dir); + keyList.put(dir, key); + } + key.add(we); + } + + /** + * WatchKey impl. + */ + private class WatchKeyImpl implements WatchService.WatchKey { + + /** + * The target. + */ + private Directory watchKey; + + /** + * The list of events. + */ + private List> list = new ArrayList>(); + + /** + * Constructor. + * + * @param theKey + */ + WatchKeyImpl(Directory theKey) { + watchKey = theKey; + } + + @Override + public void cancel() { + unregister(watchKey); + } + + @Override + public List> pollEvents() { + return list; + } + + /** + * Add an event. + * + * @param event an event + */ + void add(WatchEvent event) { + list.add(event); + } + } + + /** + * A class that depicts an event upon a directory. + */ + private class DirectoryWatchEvent implements WatchEvent { + + /** + * The type of the event. + */ + private final Kind type; + + /** + * The resource target. + */ + private final File target; + + /** + * Constructor. + * + * @param theType the kind of the event + * @param theTarget the target directory file + */ + DirectoryWatchEvent(Kind theType, File theTarget) { + type = theType; + target = theTarget; + } + + @Override + public File context() { + return target; + } + + @Override + public Kind kind() { + return type; + } + } +} diff --git a/core/src/net/sf/openrocket/util/watcher/WatchEvent.java b/core/src/net/sf/openrocket/util/watcher/WatchEvent.java new file mode 100644 index 000000000..832bc431e --- /dev/null +++ b/core/src/net/sf/openrocket/util/watcher/WatchEvent.java @@ -0,0 +1,49 @@ +package net.sf.openrocket.util.watcher; + +/** + * Mimics the JDK 7 implementation. + *

+ * the type of the context object of the event + */ +public interface WatchEvent { + + /** + * Defines the target of the event. + * + * @return the entity for which the event was generated + */ + T context(); + + /** + * Defines the type of event. + * + * @return the kind of event + */ + WatchEvent.Kind kind(); + + /** + * Defines an API for a kind of event. + * + * @param + */ + static interface Kind { + String name(); + + Class type(); + } + + /** + * A null-object idiom event. + */ + public static final WatchEvent NO_EVENT = new WatchEvent() { + @Override + public Void context() { + return null; + } + + @Override + public Kind kind() { + return null; + } + }; +} diff --git a/core/src/net/sf/openrocket/util/watcher/WatchEventKind.java b/core/src/net/sf/openrocket/util/watcher/WatchEventKind.java new file mode 100644 index 000000000..bb3b38681 --- /dev/null +++ b/core/src/net/sf/openrocket/util/watcher/WatchEventKind.java @@ -0,0 +1,75 @@ +package net.sf.openrocket.util.watcher; + +import java.io.File; + +/** + * Mimics the kind of watch event in JDK 7. + */ +public final class WatchEventKind { + + /** + * An entry was created. + */ + public static final WatchEvent.Kind ENTRY_CREATE = new WatchEvent.Kind() { + @Override + public String name() { + return "ENTRY_CREATE"; + } + + @Override + public Class type() { + return File.class; + } + + @Override + public String toString() { + return name(); + } + }; + + /** + * An existing entry was deleted. + */ + public static final WatchEvent.Kind ENTRY_DELETE = new WatchEvent.Kind() { + @Override + public String name() { + return "ENTRY_DELETE"; + } + + @Override + public Class type() { + return File.class; + } + + @Override + public String toString() { + return name(); + } + }; + + /** + * An existing entry was modified. + */ + public static final WatchEvent.Kind ENTRY_MODIFY = new WatchEvent.Kind() { + @Override + public String name() { + return "ENTRY_MODIFY"; + } + + @Override + public Class type() { + return File.class; + } + + @Override + public String toString() { + return name(); + } + }; + + /** + * Disallow instantiation. + */ + private WatchEventKind() { + } +} diff --git a/core/src/net/sf/openrocket/util/watcher/WatchService.java b/core/src/net/sf/openrocket/util/watcher/WatchService.java new file mode 100644 index 000000000..d96f49e78 --- /dev/null +++ b/core/src/net/sf/openrocket/util/watcher/WatchService.java @@ -0,0 +1,160 @@ +package net.sf.openrocket.util.watcher; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * A service that allows consumers to watch for changes to a directory or directories. This class manages the checking + * of state changes for one or more directories. Directories are the primary entity being monitored. + * Events detected include the creation of a file, creation of a subdirectory, the modification + * of a file, and the deletion of a file or subdirectory. + *

+ * JDK 7 includes a WatchService, upon which this is loosely based. This implementation is JDK 6 compatible. + *

+ * A limitation of this implementation is that it is polling based, not event driven. Also note, that this only monitors directories. If you want + * to monitor an individual file, it is recommended that you monitor the directory that the file resides within, + * then filter the WatchEvents accordingly. + * + * Example usage: + *

+ *

+ *     WatchService watcher = new WatchService();
+ *     watcher.register(new File("/tmp"));
+ *     ...
+ *     Collection changed = watcher.poll();
+ *     for (Iterator iterator = co.iterator(); iterator.hasNext(); ) {
+ *        WatchKey key = iterator.next();
+ *        //Do something with the WatchEvents in the key
+ *     }
+ *
+ *     
+ *

+ */ +public class WatchService { + + /** + * An interface that defines keys of events. + */ + public static interface WatchKey { + /** + * Cancel the registration of the directory for which this key relates. + */ + void cancel(); + + /** + * Get a list of events detected for this key. + * + * @return a list, not null, of events + */ + List> pollEvents(); + } + + /** + * The manager of all directory monitoring. + */ + private final DirectoryMonitor monitor; + + /** + * Constructor. + */ + public WatchService() { + monitor = new DirectoryMonitor(); + } + + /** + * Constructor. + * + * @param watchRecursively if true, directories will be watched recursively + */ + public WatchService(boolean watchRecursively) { + monitor = new DirectoryMonitor(watchRecursively); + } + + /** + * Polling method to get a collection of keys that indicate events detected upon one or more files within a + * monitored directory. Each key represents one directory. The list of events represents changes to one or more + * files within that directory. Will not block and return immediately, even if there are no keys ready. + * + * @return a collection of keys; guaranteed not to be null but may be empty + */ + public Collection poll() { + return monitor.check(); + } + + /** + * Polling method to get a collection of keys that indicate events detected upon one or more files within a + * monitored directory. Each key represents one directory. The list of events represents changes to one or more + * files within that directory. Will block for a defined length of time if there are no keys ready. + * + * @param time the amount of time before the method returns + * @param unit the unit of time + * + * @return a collection of keys; guaranteed not to be null but may be empty + */ + public Collection poll(long time, TimeUnit unit) { + Collection result = monitor.check(); + if (result != null && !result.isEmpty()) { + return result; + } + + try { + Thread.sleep(unit.toMillis(time)); + } catch (InterruptedException e) { + } + return monitor.check(); + } + + /** + * Blocking method to get a collection of keys that indicate events detected upon one or more files within a + * monitored directory. Each key represents one directory. The list of events represents changes to one or more + * files within that directory. + * + * @return a collection of keys; guaranteed not to be null but may be empty + */ + public Collection take() { + long wait = 60000; + Collection result = null; + do { + result = poll(wait, TimeUnit.MILLISECONDS); + } while (result == null || result.isEmpty()); + return result; + } + + /** + * Close the service and release any resources currently being used. + */ + public void close() { + monitor.close(); + } + + /** + * Register a directory to be watched by this service. The file f must not be null and must refer to a directory + * that exists. Registration is idempotent - it can be called multiple times for the same directory with no ill + * effect. + * + * @param f the target directory to watch + * + * @throws IllegalArgumentException thrown if f is null, does not exist, or is not a directory + */ + public void register(File f) throws IllegalArgumentException { + monitor.register(new Directory(f)); + } + + /** + * Register a directory to be watched by this service. The file f must not be null and must refer to a directory + * that exists. Registration is idempotent - it can be called multiple times for the same directory with no ill + * effect. + * + * @param dir the target directory to watch + * + * @throws IllegalArgumentException thrown if dir is null + */ + public void register(Directory dir) throws IllegalArgumentException { + if (dir == null) { + throw new IllegalArgumentException("The directory may not be null."); + } + monitor.register(dir); + } +} diff --git a/core/src/net/sf/openrocket/util/watcher/WatchedEventHandler.java b/core/src/net/sf/openrocket/util/watcher/WatchedEventHandler.java new file mode 100644 index 000000000..64a184857 --- /dev/null +++ b/core/src/net/sf/openrocket/util/watcher/WatchedEventHandler.java @@ -0,0 +1,33 @@ +package net.sf.openrocket.util.watcher; + +import java.util.List; + +/** + * The public contract that must be implemented by clients wanting to register an interest in, and receive notification of, + * changes to a directory or file. + */ +public interface WatchedEventHandler { + + /** + * Get the target being watched. + * + * @return a instance of a watched file + */ + W watchTarget(); + + /** + * If the target is a directory, then answer if subdirectories should also be watched for state changes. The watched target is a file, this has no + * meaning. + * + * @return true if directories are to be watched recursively (watch all subdirectories et. al.) + */ + boolean watchRecursively(); + + /** + * Callback method. This is invoked by the reactor whenever events are detected upon the target. + * + * @param theEvents a list of detected events; it's a list because if the target is a directory, potentially many files within the directory were + * affected + */ + void handleEvents(List> theEvents); +} diff --git a/core/src/net/sf/openrocket/util/watcher/WatchedFile.java b/core/src/net/sf/openrocket/util/watcher/WatchedFile.java new file mode 100644 index 000000000..40de53ea7 --- /dev/null +++ b/core/src/net/sf/openrocket/util/watcher/WatchedFile.java @@ -0,0 +1,148 @@ +package net.sf.openrocket.util.watcher; + +import java.io.File; + +/** + * Models a file on the filesystem that is being watched for state changes (other than creation). + */ +class WatchedFile { + + /** + * The last timestamp of the file. + */ + private long timeStamp; + + /** + * The file to watch. + */ + private final File target; + + /** + * Constructor. + * + * @param aFile the file to watch + * + * @throws IllegalArgumentException thrown if aFile is null + */ + WatchedFile(File aFile) throws IllegalArgumentException { + if (aFile == null) { + throw new IllegalArgumentException("The file may not be null."); + } + target = aFile; + timeStamp = target.lastModified(); + } + + /** + * Create a 'create' event. + * + * @return a watch event indicating the file was created + */ + WatchEvent createEvent() { + return new FileWatchEvent(WatchEventKind.ENTRY_CREATE); + } + + /** + * Get the watched target. + * + * @return the file being monitored + */ + File getTarget() { + return target; + } + + /** + * Detects if any changes have been made to the file. This is a 'destructive' read in the sense that it is not + * idempotent. The act of checking for changes resets the internal state and subsequent checks will indicate + * no changes until the next physical change. + * + * @return a WatchEvent instance or null if no event + */ + public final WatchEvent check() { + + if (!target.exists()) { + return new FileWatchEvent(WatchEventKind.ENTRY_DELETE); + } + + long latest = target.lastModified(); + + if (timeStamp != latest) { + timeStamp = latest; + return new FileWatchEvent(WatchEventKind.ENTRY_MODIFY); + } + return WatchEvent.NO_EVENT; + } + + /** + * Delegates existence check to the target. + * + * @return true if exists + */ + public boolean exists() { + return target.exists(); + } + + /** + * Determine equivalence to a given object. + * + * @param o another watched file + * + * @return true if the underlying file is the same + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof WatchedFile)) { + return false; + } + + final WatchedFile that = (WatchedFile) o; + + if (!target.equals(that.target)) { + return false; + } + + return true; + } + + /** + * Compute hash code. + * + * @return a hash value + */ + @Override + public int hashCode() { + return target.hashCode(); + } + + /** + * A class that depicts events that occur upon a file. + */ + protected class FileWatchEvent implements WatchEvent { + + /** + * The kind of event. + */ + private Kind type; + + /** + * Constructor. + * + * @param theType the + */ + FileWatchEvent(Kind theType) { + type = theType; + } + + @Override + public File context() { + return target; + } + + @Override + public Kind kind() { + return type; + } + } +} \ No newline at end of file diff --git a/core/test/net/sf/openrocket/util/watcher/DirectoryChangeReactorImplTest.java b/core/test/net/sf/openrocket/util/watcher/DirectoryChangeReactorImplTest.java new file mode 100644 index 000000000..2f8d9719d --- /dev/null +++ b/core/test/net/sf/openrocket/util/watcher/DirectoryChangeReactorImplTest.java @@ -0,0 +1,162 @@ +package net.sf.openrocket.util.watcher; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.util.List; + +/** + */ +public class DirectoryChangeReactorImplTest { + + @Test + public void testRegisterHandler() throws Exception { + DirectoryChangeReactorImpl impl = new DirectoryChangeReactorImpl(); + + final File tempDir = DirectoryTest.createTempDir(); + tempDir.setWritable(true); + Directory directory = new Directory(tempDir); + File f1 = null; + File f2 = null; + + File sub = null; + Directory subdir; + + try { + WatchedEventHandlerImpl testHandler = new WatchedEventHandlerImpl(directory); + impl.registerHandler(testHandler); + + String baseName = System.currentTimeMillis() + "--"; + + f1 = new File(tempDir.getAbsolutePath(), baseName + "1"); + f1.createNewFile(); + long totalSleepTime = 0; + WatchEvent.Kind kind = null; + while (totalSleepTime < DirectoryChangeReactorImpl.DEFAULT_POLLING_DELAY + 1000) { + kind = testHandler.getKind(); + if (kind != null) { + break; + } + Thread.sleep(1000); + totalSleepTime += 1000; + } + Assert.assertEquals(WatchEventKind.ENTRY_CREATE, kind); + Assert.assertEquals(testHandler.eventTarget, f1); + + f1.setLastModified(System.currentTimeMillis() + 10000); + totalSleepTime = 0; + kind = null; + while (totalSleepTime < DirectoryChangeReactorImpl.DEFAULT_POLLING_DELAY + 400) { + kind = testHandler.getKind(); + if (kind != null) { + break; + } + Thread.sleep(1000); + totalSleepTime += 1000; + } + Assert.assertEquals(WatchEventKind.ENTRY_MODIFY, kind); + Assert.assertEquals(testHandler.eventTarget, f1); + + f1.delete(); + totalSleepTime = 0; + kind = null; + while (totalSleepTime < DirectoryChangeReactorImpl.DEFAULT_POLLING_DELAY + 400) { + kind = testHandler.getKind(); + if (kind != null) { + break; + } + Thread.sleep(1000); + totalSleepTime += 1000; + } + Assert.assertEquals(WatchEventKind.ENTRY_DELETE, kind); + Assert.assertEquals(testHandler.eventTarget, f1); + + //test recursive nature of monitoring subdirectories + sub = DirectoryTest.createTempDir(tempDir); + subdir = new Directory(sub); + + totalSleepTime = 0; + kind = null; + while (totalSleepTime < DirectoryChangeReactorImpl.DEFAULT_POLLING_DELAY + 400) { + kind = testHandler.getKind(); + if (kind != null) { + break; + } + Thread.sleep(1000); + totalSleepTime += 1000; + } + Assert.assertEquals(WatchEventKind.ENTRY_CREATE, kind); + Assert.assertEquals(testHandler.eventTarget, sub); + + f2 = new File(sub.getAbsolutePath(), baseName + "2"); + f2.createNewFile(); + totalSleepTime = 0; + kind = null; + while (totalSleepTime < DirectoryChangeReactorImpl.DEFAULT_POLLING_DELAY + 400) { + kind = testHandler.getKind(); + if (kind != null) { + break; + } + Thread.sleep(1000); + totalSleepTime += 1000; + } + //Eventually, both events will show up, but it's system dependent which one we get first, and if + //they both arrive together or on different polling cycles. This could be embellished, but for now + //just see if at least one arrives. + if (kind.equals(WatchEventKind.ENTRY_CREATE)) { + Assert.assertEquals(f2, testHandler.eventTarget); + } + else if (kind.equals(WatchEventKind.ENTRY_MODIFY)) { + Assert.assertEquals(sub, testHandler.eventTarget); + } + + } + finally { + if (f1 != null) { + f1.delete(); + } + if (sub != null) { + sub.delete(); + } + directory.getTarget().delete(); + impl.shutdown(); + } + } + + static class WatchedEventHandlerImpl implements WatchedEventHandler { + Directory file = null; + Object eventTarget = null; + + WatchEvent.Kind kind = null; + + WatchedEventHandlerImpl(final Directory theFile) { + file = theFile; + } + + @Override + public Directory watchTarget() { + return file; + } + + @Override + public boolean watchRecursively() { + return true; + } + + public WatchEvent.Kind getKind() { + WatchEvent.Kind tmp = kind; + kind = null; + return tmp; + } + + @Override + public void handleEvents(final List> theEvents) { + for (int i = 0; i < theEvents.size(); i++) { + WatchEvent watchEvent = theEvents.get(i); + kind = watchEvent.kind(); + eventTarget = watchEvent.context(); + } + } + } +} diff --git a/core/test/net/sf/openrocket/util/watcher/DirectoryMonitorTest.java b/core/test/net/sf/openrocket/util/watcher/DirectoryMonitorTest.java new file mode 100644 index 000000000..1aee06e4e --- /dev/null +++ b/core/test/net/sf/openrocket/util/watcher/DirectoryMonitorTest.java @@ -0,0 +1,83 @@ +package net.sf.openrocket.util.watcher; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.util.Collection; +import java.util.List; + +/** + */ +public class DirectoryMonitorTest { + + @Test + public void testCheck() throws Exception { + final File tempDir = DirectoryTest.createTempDir(); + tempDir.setWritable(true); + Directory directory = new Directory(tempDir); + File f1 = null; + File f2 = null; + File f3 = null; + try { + + DirectoryMonitor monitor = new DirectoryMonitor(); + monitor.register(directory); + + String baseName = System.currentTimeMillis() + "--"; + + f1 = new File(tempDir.getAbsolutePath(), baseName + "1"); + f1.createNewFile(); + f2 = new File(tempDir.getAbsolutePath(), baseName + "2"); + f2.createNewFile(); + f3 = new File(tempDir.getAbsolutePath(), baseName + "3"); + f3.createNewFile(); + + Collection keys = monitor.check(); + Assert.assertEquals(1, keys.size()); + WatchService.WatchKey wk = keys.iterator().next(); + List> events = wk.pollEvents(); + + Assert.assertEquals(3, events.size()); + for (int i = 0; i < events.size(); i++) { + WatchEvent watchEvent = events.get(i); + if (watchEvent.context().equals(f1)) { + Assert.assertEquals(WatchEventKind.ENTRY_CREATE, watchEvent.kind()); + } + else if (watchEvent.context().equals(f2)) { + Assert.assertEquals(WatchEventKind.ENTRY_CREATE, watchEvent.kind()); + } + else if (watchEvent.context().equals(f3)) { + Assert.assertEquals(WatchEventKind.ENTRY_CREATE, watchEvent.kind()); + } + else { + System.err.println(watchEvent.context().toString()); + Assert.fail("Unknown target file."); + } + } + + f1.setLastModified(System.currentTimeMillis() + 10007); + f1.setReadable(true); + Thread.sleep(1000); + keys = monitor.check(); + Assert.assertEquals(1, keys.size()); + WatchService.WatchKey watchEvent = keys.iterator().next(); + Assert.assertEquals(1, watchEvent.pollEvents().size()); + Assert.assertEquals(f1, watchEvent.pollEvents().get(0).context()); + Assert.assertEquals(WatchEventKind.ENTRY_MODIFY, watchEvent.pollEvents().get(0).kind()); + } + finally { + if (f1 != null) { + f1.delete(); + } + if (f2 != null) { + f2.delete(); + } + if (f3 != null) { + f3.delete(); + } + directory.getTarget().delete(); + } + + } +} diff --git a/core/test/net/sf/openrocket/util/watcher/DirectoryTest.java b/core/test/net/sf/openrocket/util/watcher/DirectoryTest.java new file mode 100644 index 000000000..a27cf296a --- /dev/null +++ b/core/test/net/sf/openrocket/util/watcher/DirectoryTest.java @@ -0,0 +1,177 @@ +package net.sf.openrocket.util.watcher; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; + +/** + */ +public class DirectoryTest { + + @Test + public void testConstructor() throws Exception { + try { + new Directory(null); + Assert.fail(); + } + catch (IllegalArgumentException iae) { + //success + } + try { + new Directory(new File("foo")); + Assert.fail(); + } + catch (IllegalArgumentException iae) { + //success + } + } + + @Test + public void testSize() throws Exception { + final File tempDir = createTempDir(); + tempDir.setWritable(true); + Directory directory = new Directory(tempDir); + File f1 = null; + File f2 = null; + File f3 = null; + try { + Assert.assertTrue(directory.exists()); + Assert.assertEquals(0, directory.size()); + + String baseName = System.currentTimeMillis() + "--"; + + f1 = new File(tempDir.getAbsolutePath(), baseName + "1"); + f1.createNewFile(); + f2 = new File(tempDir.getAbsolutePath(), baseName + "2"); + f2.createNewFile(); + f3 = new File(tempDir.getAbsolutePath(), baseName + "3"); + f3.createNewFile(); + + Assert.assertEquals(3, directory.size()); + } + finally { + if (f1 != null) { + f1.delete(); + } + if (f2 != null) { + f2.delete(); + } + if (f3 != null) { + f3.delete(); + } + directory.getTarget().delete(); + } + } + + @Test + public void testList() throws Exception { + final File tempDir = createTempDir(); + tempDir.setWritable(true); + Directory directory = new Directory(tempDir); + File f1 = null; + File f2 = null; + File f3 = null; + try { + Assert.assertTrue(directory.exists()); + Assert.assertEquals(0, directory.size()); + + String baseName = System.currentTimeMillis() + "--"; + + f1 = new File(tempDir.getAbsolutePath(), baseName + "1"); + f1.createNewFile(); + f2 = new File(tempDir.getAbsolutePath(), baseName + "2"); + f2.createNewFile(); + f3 = new File(tempDir.getAbsolutePath(), baseName + "3"); + f3.createNewFile(); + + String[] files = directory.list(); + for (int i = 0; i < files.length; i++) { + String file = files[i]; + if (file.endsWith("1")) { + Assert.assertEquals(baseName + "1", file); + } + else if (file.endsWith("2")) { + Assert.assertEquals(baseName + "2", file); + } + else if (file.endsWith("3")) { + Assert.assertEquals(baseName + "3", file); + } + else { + Assert.fail(); + } + } + } + finally { + if (f1 != null) { + f1.delete(); + } + if (f2 != null) { + f2.delete(); + } + if (f3 != null) { + f3.delete(); + } + directory.getTarget().delete(); + } + } + + @Test + public void testContents() throws Exception { + final File tempDir = createTempDir(); + tempDir.setWritable(true); + Directory directory = new Directory(tempDir); + File f1 = null; + File f2 = null; + File f3 = null; + try { + String baseName = System.currentTimeMillis() + "--"; + + f1 = new File(tempDir.getAbsolutePath(), baseName + "1"); + f1.createNewFile(); + f2 = new File(tempDir.getAbsolutePath(), baseName + "2"); + f2.createNewFile(); + f3 = new File(tempDir.getAbsolutePath(), baseName + "3"); + f3.createNewFile(); + + Assert.assertEquals(0, directory.getContents().size()); + + //Contents is initialized at the time Directory is created. Since we had to create it for the test, + //we need a second Directory instance. + Directory directory1 = new Directory(tempDir); + Assert.assertEquals(3, directory1.getContents().size()); + } + finally { + if (f1 != null) { + f1.delete(); + } + if (f2 != null) { + f2.delete(); + } + if (f3 != null) { + f3.delete(); + } + directory.getTarget().delete(); + } + } + + //Borrowed from Google's Guava. + public static File createTempDir() { + File baseDir = new File(System.getProperty("java.io.tmpdir")); + return createTempDir(baseDir); + } + + public static File createTempDir(File parent) { + String baseName = System.currentTimeMillis() + "-"; + + for (int counter = 0; counter < 2; counter++) { + File tempDir = new File(parent, baseName + counter); + if (tempDir.mkdir()) { + return tempDir; + } + } + throw new IllegalStateException("Failed to create directory within " + + 2 + " attempts (tried " + + baseName + "0 to " + baseName + (2 - 1) + ')'); + } +} diff --git a/core/test/net/sf/openrocket/util/watcher/WatchedFileTest.java b/core/test/net/sf/openrocket/util/watcher/WatchedFileTest.java new file mode 100644 index 000000000..187648104 --- /dev/null +++ b/core/test/net/sf/openrocket/util/watcher/WatchedFileTest.java @@ -0,0 +1,82 @@ +package net.sf.openrocket.util.watcher; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; + +/** + */ +public class WatchedFileTest { + + @Test + public void testConstructor() throws Exception { + try { + new WatchedFile(null); + Assert.fail(); + } + catch (IllegalArgumentException iae) { + //success + } + + final File blah = new File("blah"); + WatchedFile wf = new WatchedFile(blah); + Assert.assertEquals(blah, wf.getTarget()); + } + + @Test + public void testCreateEvent() throws Exception { + final File blah = new File("blah"); + WatchedFile wf = new WatchedFile(blah); + Assert.assertEquals(blah, wf.createEvent().context()); + Assert.assertEquals(WatchEventKind.ENTRY_CREATE, wf.createEvent().kind()); + } + + @Test + public void testExists() throws Exception { + final File blah = new File("blah"); + WatchedFile wf = new WatchedFile(blah); + Assert.assertFalse(wf.exists()); + } + + @Test + public void testCheck() throws Exception { + final File blah = new File("blah"); + WatchedFile wf = new WatchedFile(blah); + + WatchEvent check = wf.check(); + Assert.assertEquals(WatchEventKind.ENTRY_DELETE, check.kind()); + + File f = File.createTempFile("tmp", "tmp"); + wf = new WatchedFile(f); + + check = wf.check(); + Assert.assertEquals(WatchEvent.NO_EVENT, check); + + f.setLastModified(System.currentTimeMillis() - 60000); + check = wf.check(); + Assert.assertEquals(WatchEventKind.ENTRY_MODIFY, check.kind()); + Assert.assertEquals(f, check.context()); + + //Check for reset of state + check = wf.check(); + Assert.assertEquals(WatchEvent.NO_EVENT, check); + } + + @Test + public void testEquals() throws Exception { + final File blah = new File("blah"); + final File blech = new File("blech"); + WatchedFile wf1 = new WatchedFile(blah); + WatchedFile wf2 = new WatchedFile(blah); + WatchedFile wf3 = new WatchedFile(blech); + + Assert.assertEquals(wf1, wf1); + Assert.assertEquals(wf1, wf2); + Assert.assertFalse(wf1.equals(wf3)); + Assert.assertFalse(wf1.equals(null)); + Assert.assertFalse(wf1.equals(new Object())); + + Assert.assertEquals(wf1.hashCode(), wf2.hashCode()); + } +}