Merge pull request #25 from rodinia814/watcher

Directory/File Watcher
This commit is contained in:
plaa 2013-01-04 14:52:03 -08:00
commit e2e509210c
14 changed files with 1550 additions and 5 deletions

View File

@ -1,9 +1,11 @@
package net.sf.openrocket.startup;
import com.google.inject.AbstractModule;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.logging.LogHelper;
import com.google.inject.AbstractModule;
import net.sf.openrocket.util.watcher.DirectoryChangeReactor;
import net.sf.openrocket.util.watcher.DirectoryChangeReactorImpl;
public class ApplicationModule extends AbstractModule {
@ -12,6 +14,7 @@ public class ApplicationModule extends AbstractModule {
bind(LogHelper.class).toInstance(Application.getLogger());
bind(Preferences.class).toInstance(Application.getPreferences());
bind(Translator.class).toInstance(Application.getTranslator());
bind(DirectoryChangeReactor.class).to(DirectoryChangeReactorImpl.class);
}
}

View File

@ -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<String, WatchedFile> contents = new HashMap<String, WatchedFile>();
/**
* 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<String, WatchedFile> getContents() {
return contents;
}
/**
* Shared lock.
*
* @return a lock
*/
protected final Object getLock() {
return lock;
}
}

View File

@ -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.
* <p/>
* 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<Directory> theEventHandler);
/**
* Unregister an event handler with the reactor.
*
* @param theEventHandler the handler
*/
void unregisterHandler(WatchedEventHandler<Directory> theEventHandler);
}

View File

@ -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).
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* For example, to change the interval to 10 seconds: -Dopenrocket.watcher.poll=10000
* <p/>
* <p/>
* Example Usage of this class:
* <pre>
* <code>
*
* class MyHandler extends WatchedEventHandler<Directory> {
*
* private Directory dirToWatch = new Directory(new File("/tmp"));
*
* public Directory watchTarget() {
* return dirToWatch;
* }
*
* public boolean watchRecursively() {
* return true;
* }
*
* public void handleEvents(List<WatchEvent<?>> theEvents) {
* for (int i = 0; i < events.size(); i++) {
* WatchEvent<?> watchEvent = events.get(i);
* // process the event
* }
* }
* }
* </code>
*
* 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());
* </pre>
*/
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<WatchedEventHandler, WatchService> activeWatchers = new ConcurrentHashMap<WatchedEventHandler, WatchService>();
/**
* 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<WatchedEventHandler> iterator = activeWatchers.keySet().iterator(); iterator.hasNext(); ) {
WatchedEventHandler next = iterator.next();
activeWatchers.get(next).close();
iterator.remove();
}
}
}
@Override
public void registerHandler(WatchedEventHandler<Directory> 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<Directory> 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<WatchedEventHandler> 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();
}
}
}
}

View File

@ -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.
* <p/>
* Synchronize calls to this class externally to ensure thread safety.
*/
final class DirectoryMonitor {
/**
* The directories being monitored.
*/
private Set<Directory> monitored = new HashSet<Directory>();
/**
* 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<Directory, WatchKeyImpl> result = new HashMap<Directory, WatchKeyImpl>();
for (Directory directory : monitored) {
synchronized (directory.getLock()) {
if (directory.exists()) {
Map<String, WatchedFile> watchedFiles = (Map<String, WatchedFile>) 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<Directory, WatchKeyImpl> 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<WatchEvent<?>> list = new ArrayList<WatchEvent<?>>();
/**
* Constructor.
*
* @param theKey
*/
WatchKeyImpl(Directory theKey) {
watchKey = theKey;
}
@Override
public void cancel() {
unregister(watchKey);
}
@Override
public List<WatchEvent<?>> 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<File> {
/**
* The type of the event.
*/
private final Kind<File> type;
/**
* The resource target.
*/
private final File target;
/**
* Constructor.
*
* @param theType the kind of the event
* @param theTarget the target directory file
*/
DirectoryWatchEvent(Kind<File> theType, File theTarget) {
type = theType;
target = theTarget;
}
@Override
public File context() {
return target;
}
@Override
public Kind<File> kind() {
return type;
}
}
}

View File

@ -0,0 +1,49 @@
package net.sf.openrocket.util.watcher;
/**
* Mimics the JDK 7 implementation.
* <p/>
* <T> the type of the context object of the event
*/
public interface WatchEvent<T> {
/**
* 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<T> kind();
/**
* Defines an API for a kind of event.
*
* @param <T>
*/
static interface Kind<T> {
String name();
Class<T> type();
}
/**
* A null-object idiom event.
*/
public static final WatchEvent<Void> NO_EVENT = new WatchEvent<Void>() {
@Override
public Void context() {
return null;
}
@Override
public Kind<Void> kind() {
return null;
}
};
}

View File

@ -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<File> ENTRY_CREATE = new WatchEvent.Kind<File>() {
@Override
public String name() {
return "ENTRY_CREATE";
}
@Override
public Class<File> type() {
return File.class;
}
@Override
public String toString() {
return name();
}
};
/**
* An existing entry was deleted.
*/
public static final WatchEvent.Kind<File> ENTRY_DELETE = new WatchEvent.Kind<File>() {
@Override
public String name() {
return "ENTRY_DELETE";
}
@Override
public Class<File> type() {
return File.class;
}
@Override
public String toString() {
return name();
}
};
/**
* An existing entry was modified.
*/
public static final WatchEvent.Kind<File> ENTRY_MODIFY = new WatchEvent.Kind<File>() {
@Override
public String name() {
return "ENTRY_MODIFY";
}
@Override
public Class<File> type() {
return File.class;
}
@Override
public String toString() {
return name();
}
};
/**
* Disallow instantiation.
*/
private WatchEventKind() {
}
}

View File

@ -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.
* <p/>
* JDK 7 includes a WatchService, upon which this is loosely based. This implementation is JDK 6 compatible.
* <p/>
* 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:
* <p>
* <pre>
* WatchService watcher = new WatchService();
* watcher.register(new File("/tmp"));
* ...
* Collection<? extends WatchKey> changed = watcher.poll();
* for (Iterator<WatchKey> iterator = co.iterator(); iterator.hasNext(); ) {
* WatchKey key = iterator.next();
* //Do something with the WatchEvents in the key
* }
*
* </pre>
* </p>
*/
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<WatchEvent<?>> 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);
}
}

View File

@ -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<W extends WatchedFile> {
/**
* 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<WatchEvent<?>> theEvents);
}

View File

@ -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<File> 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<File> {
/**
* The kind of event.
*/
private Kind<File> type;
/**
* Constructor.
*
* @param theType the
*/
FileWatchEvent(Kind<File> theType) {
type = theType;
}
@Override
public File context() {
return target;
}
@Override
public Kind<File> kind() {
return type;
}
}
}

View File

@ -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> {
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<WatchEvent<?>> theEvents) {
for (int i = 0; i < theEvents.size(); i++) {
WatchEvent<?> watchEvent = theEvents.get(i);
kind = watchEvent.kind();
eventTarget = watchEvent.context();
}
}
}
}

View File

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

View File

@ -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) + ')');
}
}

View File

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