/*
* Copyright 2013 The Http Server & Proxy
*
* The Http Server & Proxy Project licenses this file to you under the Apache License, version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
package com.sohail.alam.http.common.watchservice;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static com.sohail.alam.http.common.LoggerManager.LOGGER;
/**
* User: Sohail Alam
* Version: 1.0.0
* Date: 25/9/13
* Time: 8:56 AM
*/
public class WatchService {
public long repeatInterval = 1;
public TimeUnit timeUnit = TimeUnit.SECONDS;
/**
* Private Constructor
*/
private WatchService() {
}
/**
* Watcher watch service.
*
* @return the watch service
*/
public static WatchService watcher() {
return SingletonHolder.INSTANCE;
}
/**
* Add callback listener.
*
* @param listener the listener
*
* @throws WatchServiceException the watch service exception
*/
public <T extends WatchServiceCallback> void addCallbackListener(T listener) throws WatchServiceException {
// If the listener to be added implements DirectoryService then add it to DIR_CALLBACK_LISTENERS
if (listener instanceof WatchServiceCallback.DirectoryService) {
SingletonHolder.DIR_CALLBACK_LISTENERS.get().add((WatchServiceCallback.DirectoryService) listener);
}
// If the listener to be added implements FileService then add it to FILE_CALLBACK_LISTENERS
else if (listener instanceof WatchServiceCallback.FileService) {
SingletonHolder.FILE_CALLBACK_LISTENERS.get().add((WatchServiceCallback.FileService) listener);
}
// If the listener to be added simply implements WatchServiceCallback, then throw Exception
else {
throw new WatchServiceException("Listener MUST either implement DirectoryService or FileService type of WatchServiceCallback");
}
}
/**
* Remove the appropriate callback listener.
*
* @param listener the listener
*/
public <T extends WatchServiceCallback> void removeCallbackListener(T listener) {
if (listener instanceof WatchServiceCallback.DirectoryService) {
SingletonHolder.DIR_CALLBACK_LISTENERS.get().remove(listener);
} else if (listener instanceof WatchServiceCallback.FileService) {
SingletonHolder.FILE_CALLBACK_LISTENERS.get().remove(listener);
}
}
/**
* Start watching a particular file.
* <p/>
* Here the reference to 'file' can be equally understood as a 'file' or 'directory'.
* <p/>
* To receive any callback events, the callback handlers MUST be
* added BEFORE starting to watch service for the file.
* <p/>
* If the file to be watched already has a watch service running then a
* {@link WatchServiceCallback#alreadyWatchingFile(java.io.File)} callback
* is provided.
* <p/>
* This is a fail first kind of method.
* <p/>
* If the file which is to be watched does not exists at the time of execution of this method,
* then an {@link WatchServiceCallback#exceptionCaught(java.io.File, Throwable)}
* callback will be provided. You will NOT be notified further about this
* non existing file, however, if the file existed at the time of starting the watch service
* and was deleted/moved after starting the watch service, then an appropriate callback is provided -
* {@link WatchServiceCallback.DirectoryService#directoryNoLongerExists(java.io.File, long)} or
* {@link WatchServiceCallback.FileService#fileNoLongerExists(java.io.File, long)}.
*
* @param file the file/directory
*/
public void watch(File file) {
if (!file.exists()) {
provideCallbackExceptionCaught(file, new FileNotFoundException("The file does not exists: " + file.getName()));
return;
}
if (SingletonHolder.WATCHER_MAP.get(file) == null) {
SingletonHolder.WATCHER_MAP.put(file, file.lastModified());
startWatching(file);
} else {
provideCallbackAlreadyWatching(file);
}
}
/**
* Gets directory service callback listeners.
*
* @return the directory service callback listeners
*/
private List<WatchServiceCallback.DirectoryService> getDirServiceCallbackListeners() {
return SingletonHolder.DIR_CALLBACK_LISTENERS.get();
}
/**
* Gets file service callback listeners.
*
* @return the file service callback listeners
*/
private List<WatchServiceCallback.FileService> getFileServiceCallbackListeners() {
return SingletonHolder.FILE_CALLBACK_LISTENERS.get();
}
/**
* Start watching.
*
* @param file the file
*/
private void startWatching(File file) {
if (file.isFile()) {
ScheduledFuture future = SingletonHolder.WATCH_SERVICE.
scheduleAtFixedRate(new FileWatchTask(file), 0, repeatInterval, timeUnit);
SingletonHolder.WATCH_SERVICE_FUTURE.put(file, future);
} else if (file.isDirectory()) {
ScheduledFuture future = SingletonHolder.WATCH_SERVICE.
scheduleAtFixedRate(new DirWatchTask(file), 0, repeatInterval, timeUnit);
SingletonHolder.WATCH_SERVICE_FUTURE.put(file, future);
}
}
/**
* Provide callback already watching.
*
* @param file the file
*/
private void provideCallbackAlreadyWatching(File file) {
if (file.isDirectory()) {
for (WatchServiceCallback callback : getDirServiceCallbackListeners()) {
callback.alreadyWatchingFile(file);
}
} else if (file.isFile()) {
for (WatchServiceCallback callback : getFileServiceCallbackListeners()) {
callback.alreadyWatchingFile(file);
}
}
}
/**
* Provide callback exception caught.
*
* @param file the file
* @param cause the cause
*/
private void provideCallbackExceptionCaught(File file, Throwable cause) {
if (file.isDirectory()) {
for (WatchServiceCallback callback : getDirServiceCallbackListeners()) {
callback.exceptionCaught(file, cause);
}
} else if (file.isFile()) {
for (WatchServiceCallback callback : getFileServiceCallbackListeners()) {
callback.exceptionCaught(file, cause);
}
}
}
/**
* Provide callback directory modified.
*
* @param directory the directory
* @param oldLastModified the old last modified
* @param newLastModified the new last modified
*/
private void provideCallbackDirectoryModified(File directory, long oldLastModified, long newLastModified) {
for (WatchServiceCallback.DirectoryService callback : getDirServiceCallbackListeners()) {
callback.directoryModified(directory, oldLastModified, newLastModified);
}
}
/**
* Provide callback directory no longer exists.
*
* @param file the file
* @param oldLastModified the old last modified
*/
private void provideCallbackDirectoryNoLongerExists(File file, long oldLastModified) {
for (WatchServiceCallback.DirectoryService callback : getDirServiceCallbackListeners()) {
callback.directoryNoLongerExists(file, oldLastModified);
}
}
/**
* Provide callback new directory added.
*
* @param file the file
* @param oldNumberOfItems the old number of items
* @param newNumberOfItems the new number of items
*/
private void provideCallbackDirectoryCountChanged(File file, long oldNumberOfItems, long newNumberOfItems) {
for (WatchServiceCallback.DirectoryService callback : getDirServiceCallbackListeners()) {
callback.directoryCountChanged(file, oldNumberOfItems, newNumberOfItems);
}
}
/**
* Provide callback new file added.
*
* @param file the file
* @param oldNumberOfItems the old number of items
* @param newNumberOfItems the new number of items
*/
private void provideCallbackFileCountChanged(File file, long oldNumberOfItems, long newNumberOfItems) {
for (WatchServiceCallback.DirectoryService callback : getDirServiceCallbackListeners()) {
callback.fileCountChanged(file, oldNumberOfItems, newNumberOfItems);
}
}
/**
* Provide callback file modified.
*
* @param file the file
* @param oldLastModified the old last modified
* @param newLastModified the new last modified
*/
private void provideCallbackFileModified(File file, long oldLastModified, long newLastModified) {
for (WatchServiceCallback.FileService callback : getFileServiceCallbackListeners()) {
callback.fileModified(file, oldLastModified, newLastModified);
}
}
/**
* Provide callback file no longer exists.
*
* @param file the file
* @param lastModified the last modified
*/
private void provideCallbackFileNoLongerExists(File file, long lastModified) {
for (WatchServiceCallback.FileService callback : getFileServiceCallbackListeners()) {
callback.fileNoLongerExists(file, lastModified);
}
}
/**
* The interface Singleton holder.
*/
private static interface SingletonHolder {
public static final WatchService INSTANCE
= new WatchService();
public static final ScheduledThreadPoolExecutor WATCH_SERVICE
= new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors() * 2);
public static final Map<File, ScheduledFuture> WATCH_SERVICE_FUTURE
= new HashMap<File, ScheduledFuture>();
public static final Map<File, Long> WATCHER_MAP
= new ConcurrentHashMap<File, Long>();
public static final AtomicReference<List<WatchServiceCallback.DirectoryService>> DIR_CALLBACK_LISTENERS
= new AtomicReference<List<WatchServiceCallback.DirectoryService>>(new ArrayList<WatchServiceCallback.DirectoryService>());
public static final AtomicReference<List<WatchServiceCallback.FileService>> FILE_CALLBACK_LISTENERS
= new AtomicReference<List<WatchServiceCallback.FileService>>(new ArrayList<WatchServiceCallback.FileService>());
}
/**
* The type Dir watch task.
*/
private class DirWatchTask implements Runnable {
private File file;
private Long oldLastModified;
private long numberOfDirItems = 0;
private long numberOfFileItems = 0;
/**
* Instantiates a new Dir watch task.
*
* @param file the file
*/
public DirWatchTask(File file) {
this.file = file;
this.oldLastModified = file.lastModified();
countItems();
}
/**
* Count items.
*/
private void countItems() {
File[] all = this.file.listFiles();
if (all != null) {
this.numberOfDirItems = 0;
this.numberOfFileItems = 0;
for (File file : all) {
if (file.isDirectory())
this.numberOfDirItems++;
else if (file.isFile())
this.numberOfFileItems++;
}
} else {
this.numberOfDirItems = 0;
this.numberOfFileItems = 0;
}
}
/**
* Run void.
*/
@Override
public void run() {
if (file.exists()) {
long newLastModified = file.lastModified();
long oldNumberOfDirItems = this.numberOfDirItems;
long oldNumberOfFileItems = this.numberOfFileItems;
if ((oldLastModified = SingletonHolder.WATCHER_MAP.get(this.file)) != null) {
if (newLastModified > oldLastModified) {
SingletonHolder.WATCHER_MAP.put(file, newLastModified);
WatchService.watcher().provideCallbackDirectoryModified(file, oldLastModified, newLastModified);
countItems();
if (oldNumberOfDirItems != this.numberOfDirItems)
WatchService.watcher().provideCallbackDirectoryCountChanged(file, oldNumberOfDirItems, this.numberOfDirItems);
if (oldNumberOfFileItems != this.numberOfFileItems)
WatchService.watcher().provideCallbackFileCountChanged(file, oldNumberOfFileItems, this.numberOfFileItems);
}
} else {
WatchService.watcher().provideCallbackExceptionCaught(file,
new WatchServiceException("Directory seems to be missing from WatchService!"));
}
} else {
WatchService.watcher().provideCallbackDirectoryNoLongerExists(file, oldLastModified);
SingletonHolder.WATCHER_MAP.remove(file);
SingletonHolder.WATCH_SERVICE_FUTURE.remove(file).cancel(false);
SingletonHolder.WATCH_SERVICE.remove(this);
SingletonHolder.WATCH_SERVICE.purge();
}
}
}
/**
* The type File watch task.
*/
private class FileWatchTask implements Runnable {
private File file;
private Long oldLastModified;
/**
* Instantiates a new File watch task.
*
* @param file the file
*/
public FileWatchTask(File file) {
this.file = file;
this.oldLastModified = file.lastModified();
LOGGER.debug("Watching File for modification: {}", file.getAbsoluteFile());
}
/**
* Run void.
*/
@Override
public void run() {
if (file.exists()) {
long newLastModified = file.lastModified();
if ((oldLastModified = SingletonHolder.WATCHER_MAP.get(this.file)) != null) {
if (newLastModified > oldLastModified) {
SingletonHolder.WATCHER_MAP.put(file, newLastModified);
WatchService.watcher().provideCallbackFileModified(file, oldLastModified, newLastModified);
}
} else {
WatchService.watcher().provideCallbackExceptionCaught(file,
new WatchServiceException("File seems to be missing from WatchService!"));
}
} else {
WatchService.watcher().provideCallbackFileNoLongerExists(file, oldLastModified);
SingletonHolder.WATCHER_MAP.remove(file);
SingletonHolder.WATCH_SERVICE_FUTURE.remove(file).cancel(false);
SingletonHolder.WATCH_SERVICE.remove(this);
SingletonHolder.WATCH_SERVICE.purge();
}
}
}
}