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 extends WatchService.WatchKey> 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 extends WatchService.WatchKey> 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 extends WatchKey> 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 extends WatchKey> 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 extends WatchKey> poll(long time, TimeUnit unit) {
+ Collection extends WatchKey> 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 extends WatchKey> take() {
+ long wait = 60000;
+ Collection extends WatchKey> 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 extends WatchService.WatchKey> 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());
+ }
+}