package com.github.drapostolos.rdp4j;
import static com.github.drapostolos.rdp4j.DirectoryPollerBuilder.DEFAULT_THREAD_NAME;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import com.github.drapostolos.rdp4j.spi.PolledDirectory;
/**
* The DirectoryPoller, used for adding/removing {@link Rdp4jListener}s/{@link PolledDirectory}'s
* and terminating the poller mechanism.
* <p>
* Simple usage example:
* <pre>
* DirectoryPoller dp = DirectoryPoller.newBuilder()
* .addDirectory(new MyPolledDirectoryImp(...))
* .addListener(new MyListenerImp())
* .setPollingInterval(2, TimeUnit.SECONDS) // optional, 1 second default.
* .setFileFilter(new MyFileFilterImp()) // optional, default matches all files.
* .start();
*
* // do something
*
* dp.stop();
*
* </pre>
*
* @see <a href="https://github.com/drapostolos/rdp4j/wiki/User-Guide">User-Guide</a>
*/
public class DirectoryPoller {
private static final String NULL_ARGUMENT_ERROR = "Argument is null.";
private static final long WITH_NO_DELAY = 0;
private static AtomicInteger threadCount = new AtomicInteger();
private volatile boolean shouldInvokeShutdownTask = true;
private ScheduledRunnable scheduledRunnable;
private FileFilter filter;
private long pollingIntervalInMillis;
private String threadName;
private CountDownLatch latch = new CountDownLatch(1);
private ScheduledExecutorService executor;
// Below are passed to PollerTask
ListenerNotifier notifier;
boolean fileAddedEventEnabledForInitialContent;
boolean parallelDirectoryPollingEnabled;
Set<PolledDirectory> directories;
/* package-private access only */
DirectoryPoller(DirectoryPollerBuilder builder) {
// First copy values from builder...
directories = new LinkedHashSet<PolledDirectory>(builder.directories);
filter = builder.filter;
pollingIntervalInMillis = builder.pollingIntervalInMillis;
threadName = builder.threadName;
fileAddedEventEnabledForInitialContent = builder.fileAddedEventEnabledForInitialContent;
parallelDirectoryPollingEnabled = builder.parallelDirectoryPollingEnabled;
notifier = new ListenerNotifier(builder.listeners);
// ...then check mandatory values
if (directories.isEmpty()) {
String pollerName = DirectoryPoller.class.getSimpleName();
String builderName = DirectoryPollerBuilder.class.getSimpleName();
String message = "Unable to start the '%s' when No directories has been added! "
+ "You must add at least one directory before starting the '%s'.\n"
+ "Call this method to add a directory: %s.addPolledDirectory(PolledDirectory), "
+ "before you can start the %s.";
throw new IllegalStateException(String.format(message, pollerName, pollerName, builderName, pollerName));
}
if (threadName.equals(DEFAULT_THREAD_NAME)) {
threadName = threadName + threadCount.incrementAndGet();
}
scheduledRunnable = new ScheduledRunnable(this);
}
/**
* @return a new {@link DirectoryPollerBuilder}.
*/
public static DirectoryPollerBuilder newBuilder() {
return new DirectoryPollerBuilder();
}
void start() {
executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(threadName);
return t;
}
});
executor.scheduleAtFixedRate(scheduledRunnable, WITH_NO_DELAY, pollingIntervalInMillis, MILLISECONDS);
}
/**
* Stops the polling mechanism. There will be no more new poll-cycles after this method has
* returned. If this method is called during a poll-cycle it will block and wait for current
* poll-cycle to finish gracefully (This also includes waiting for any listener methods
* to finish gracefully).
* <p>
* Furthermore, it will block until all notifications to
* {@link DirectoryPollerListener#afterStop(AfterStopEvent)} has been processed.
* <p>
* If a previous call has been made to {@link #stopAsync()}, {@link #stopAsyncNow()} and
* the last poll-cycle is not finished, then this method will block until the above
* conditions are met.
* <p>
* Subsequent calls to this method have no affect.
*/
public void stop() {
stopAsync();
awaitTermination();
}
/**
* Stops the polling mechanism. There will be no more new poll-cycles after this method has
* returned. If this method is called during a poll-cycle it will block and wait for current
* poll-cycle to finish. Underlying threads will be interrupted (this also includes any
* listener methods).
* <p>
* Furthermore, it will block until all notifications to
* {@link DirectoryPollerListener#afterStop(AfterStopEvent)} has been processed.
* <p>
* If a previous call has been made to {@link #stopAsync()}, {@link #stopAsyncNow()} and
* the last poll-cycle is not finished, then this method will block until the above
* conditions are met.
* <p>
* Subsequent calls to this method have no affect.
*/
public void stopNow() {
stopAsyncNow();
awaitTermination();
}
/**
* Stops the polling mechanism, but does not wait for any ongoing poll-cycles to finish.
* Any ongoing poll-cycles will finish gracefully. Use the following method to wait for
* termination: {@link #awaitTermination()}.
* <p>
* Subsequent calls to this method have no affect.
*/
public void stopAsync() {
executor.shutdown();
invokeShutdownTaskOnce();
}
/**
* Stops the polling mechanism, but does not wait for any ongoing poll-cycles to finish.
* Underlying threads will be interrupted (this also includes any
* listener methods). Use the following method to wait for termination:
* {@link #awaitTermination()}.
* <p>
* Subsequent calls to this method have no affect.
*/
public void stopAsyncNow() {
executor.shutdownNow();
invokeShutdownTaskOnce();
}
private synchronized void invokeShutdownTaskOnce() {
if (shouldInvokeShutdownTask) {
final DirectoryPoller dp = this;
Util.invokeTask("DP-AfterStop", new Callable<Void>() {
@Override
public Void call() {
Util.awaitTermination(executor);
scheduledRunnable.shutdown();
scheduledRunnable.awaitTermination();
notifier.afterStop(new AfterStopEvent(dp));
latch.countDown();
return null;
}
});
}
shouldInvokeShutdownTask = false;
}
/**
* Blocks until the last poll-cycle has finished and all {@link AfterStopEvent} has been
* processed.
*/
public void awaitTermination() {
try {
latch.await();
} catch (InterruptedException e) {
String message = "awaitTermination() method was interrupted!";
throw new UnsupportedOperationException(message, e);
}
}
/**
* @return {@code true} if this {@link DirectoryPoller} is terminated, otherwise {@code false}.
*/
public boolean isTerminated() {
return latch.getCount() == 0;
}
/**
* @return the current {@link PolledDirectory}'s handled by
* this instance.
*/
public Set<PolledDirectory> getPolledDirectories() {
return scheduledRunnable.getDirectories();
}
/**
* @return the polling interval in milliseconds, as
* configured for this instance
*/
public long getPollingIntervalInMillis() {
return pollingIntervalInMillis;
}
/**
* @return the default {@link FileFilter}, as configured for this
* instance.
*/
public FileFilter getDefaultFileFilter() {
return filter;
}
/**
* Adds the given <code>listener</code> into this instance.
* The <code>listener</code> will start receiving notifications
* in the next coming poll-cycle.
* <p>
* Any {@link DirectoryPollerListener} added with this method
* will not have it's {@link DirectoryPollerListener#beforeStart(BeforeStartEvent)}
* method triggered, as at this point the {@link DirectoryPoller}
* is already started.
* <p>
* Registering an already registered listener will be ignored.
*
* @throws NullPointerException if given <code>listener</code> is null.
* @param listener implementation of any of the sub-interfaces of {@link Rdp4jListener}.
*/
public void addListener(Rdp4jListener listener) {
if (listener == null) {
throw new NullPointerException(NULL_ARGUMENT_ERROR);
}
scheduledRunnable.addListener(listener);
}
/**
* Removes the given <code>listener</code> from this instance.
* The <code>listener</code> will be removed after any ongoing
* poll-cycles has finished. If there's no ongoing poll-cycles,
* then the given <code>listener</code> will be removed just before
* next poll-cycle is started.
*
* @param listener implementation of any of the sub-interfaces of {@link Rdp4jListener}
* -interface.
* @throws NullPointerException if the given argument is null.e
*/
public void removeListener(Rdp4jListener listener) {
if (listener == null) {
throw new NullPointerException(NULL_ARGUMENT_ERROR);
}
scheduledRunnable.removeListener(listener);
}
/**
* Adds the given <code>directory</code> into this instance.
* The <code>directory</code> will be polled in the next coming poll-cycle.
* <p>
* Registering an already registered directory will be ignored.
*
* @param directory implementation of {@link PolledDirectory}.
* @throws NullPointerException if the given argument is null.
*/
public void addPolledDirectory(PolledDirectory directory) {
if (directory == null) {
throw new NullPointerException(NULL_ARGUMENT_ERROR);
}
scheduledRunnable.addDirectory(directory);
}
/**
* Removes the given <code>directory</code> from this instance.
* The <code>directory</code> will be removed after any ongoing
* poll-cycles has finished. If there's no ongoing poll-cycles,
* then the given <code>directory</code> will be removed just before
* next poll-cycle is started.
*
* @param directory implementation of {@link PolledDirectory}.
* @throws NullPointerException if the given argument is null.
*/
public void removePolledDirectory(PolledDirectory directory) {
if (directory == null) {
throw new NullPointerException(NULL_ARGUMENT_ERROR);
}
scheduledRunnable.removeDirectory(directory);
}
/**
* Returns a string representation of this Directory monitor,
* in the format: "{thread-name}: {directories} [polling every {polling-interval} milliseconds]"
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder()
.append(getThreadName())
.append(": ")
.append(getPolledDirectories())
.append(" [polling every: ")
.append(getPollingIntervalInMillis())
.append(" milliseconds]");
return sb.toString();
}
/**
* @return the name of the associated polling thread.
*/
public String getThreadName() {
return threadName;
}
/**
* @return <code>true</code> if this {@link DirectoryPoller} has
* been configured to poll its directories in parallel, otherwise
* return false.
*/
public boolean isParallelDirectoryPollingEnabled() {
return parallelDirectoryPollingEnabled;
}
/**
* @return <code>true</code> if this {@link DirectoryPoller} has
* been configured to notify {@link DirectoryListener#fileAdded(FileAddedEvent)}
* method for the initial content of its directories, otherwise
* returns false.
* <p>
* The initial content of a directory are the files/directories
* it contains the first poll-cycle.
*/
public boolean isFileAdedEventForInitialContentEnabled() {
return fileAddedEventEnabledForInitialContent;
}
}