package org.rr.jeborker.app; import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; import java.io.File; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.logging.Level; import javax.swing.SwingUtilities; import org.rr.commons.log.LoggerFactory; import org.rr.commons.mufs.IResourceHandler; import org.rr.commons.mufs.ResourceHandlerFactory; import org.rr.commons.utils.ReflectionUtils; import org.rr.jeborker.Jeboorker; import org.rr.jeborker.app.preferences.PreferenceStoreFactory; import org.rr.jeborker.db.DefaultDBManager; import org.rr.jeborker.db.item.EbookPropertyItem; import org.rr.jeborker.db.item.EbookPropertyItemUtils; import org.rr.jeborker.gui.MainController; import org.rr.jeborker.gui.action.ActionUtils; public class FileWatchService { private static WatchService watchService; private static final HashMap<String, WatchKey> items = new HashMap<String, WatchKey>(); static { try { watchService = FileSystems.getDefault().newWatchService(); Jeboorker.APPLICATION_THREAD_POOL.submit(new WatchFolderRunnable()); } catch (IOException e) { LoggerFactory.getLogger(FileWatchService.class).log(Level.WARNING, "Failed to add file watch service", e); } } private FileWatchService() { } /** * Adds the given folders to the watched ones. */ public static void addWatchPath(String path) { addWatchPath(Collections.singletonList(path)); } /** * Removes the given path from the watch * @param path The path to be removed from watch. */ public static void removeWatchPath(String path) { path = new File(path).getAbsolutePath(); WatchKey watchKey = items.remove(path); if(watchKey != null) { watchKey.cancel(); LoggerFactory.getLogger(FileWatchService.class).log(Level.INFO, "Removing " + path + " from watch service."); } } /** * Adds the given folders to the watched ones. */ public static void addWatchPath(final Collection<String> p) { LoggerFactory.getLogger(FileWatchService.class).log(Level.INFO, "Adding " + p.size() + " folders to watch service."); for(String path : p) { try { path = new File(path).getAbsolutePath(); File pathFile = new File(path); if(!isAlreadyWatched(path) && pathFile.isDirectory()) { WatchKey watchKey = Paths.get(path).register(watchService, new Kind<?>[] { ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE }); items.put(path, watchKey); LoggerFactory.getLogger(FileWatchService.class).log(Level.INFO, "Added " + path + " to watch service."); } } catch (Exception e) { LoggerFactory.getLogger(FileWatchService.class).log(Level.WARNING, "Failed to add path " + path + " to file watch service. Stopping to add watches.", e); break; } } } /** * Shutdown the watch service. No file change is detected after shutting down the service. */ public static void shutdownWatchService() { for(WatchKey key : items.values()) { key.cancel(); } items.clear(); } /** * Tells if the given path is already under watch. */ private static boolean isAlreadyWatched(final String path) { return items.containsKey(path); } private static class WatchFolderRunnable implements Runnable { @Override public void run() { while (true) { try { WatchKey watchKey = watchService.take(); final List<EbookPropertyItem> changedEbooks = new ArrayList<>(); final List<IResourceHandler> addedResources = new ArrayList<>(); for (WatchEvent<?> watchEvent : watchKey.pollEvents()) { if(!FileRefreshBackground.isDisabled()) { final Path fullPath = ((Path)watchKey.watchable()).resolve((Path)watchEvent.context()); final IResourceHandler resourceHandler = ResourceHandlerFactory.getResourceHandler(fullPath.toFile()); final List<EbookPropertyItem> ebookPropertyItemByResource = EbookPropertyItemUtils.getEbookPropertyItemByResource(resourceHandler); if(ebookPropertyItemByResource.isEmpty() && watchEvent.kind() == ENTRY_CREATE) { addedResources.add(resourceHandler); } else if(watchEvent.kind() == ENTRY_DELETE || watchEvent.kind() == ENTRY_MODIFY) { changedEbooks.addAll(ebookPropertyItemByResource); } } } watchKey.reset(); FileRefreshBackground.runWithDisabledRefresh(new Runnable() { @Override public void run() { //seems the file may be not ready to read, wait... ReflectionUtils.sleepSilent(500); transferDeleteAndRefresh(changedEbooks); transferNewEbookFiles(addedResources); } }); } catch(Exception e) { LoggerFactory.getLogger(FileWatchService.class).log(Level.WARNING, "WatchFolderRunnable", e); } } } private void transferNewEbookFiles(final List<IResourceHandler> addedResources) { for (IResourceHandler resource : addedResources) { if(EbookPropertyItemUtils.getEbookPropertyItemByResource(resource).isEmpty()) { IResourceHandler basePathForFile = PreferenceStoreFactory.getPreferenceStore(PreferenceStoreFactory.DB_STORE).getBasePath().getBasePathForFile(resource); if( basePathForFile != null && resource.exists() && ActionUtils.isSupportedEbookFormat(resource, true) ) { final EbookPropertyItem item = EbookPropertyItemUtils.createEbookPropertyItem(resource, basePathForFile); DefaultDBManager.getInstance().storeObject(item); ActionUtils.addAndStoreEbookPropertyItem(item); MainController.getController().getMainTreeHandler().refreshFileSystemTreeEntry(basePathForFile); LoggerFactory.getLogger().log(Level.INFO, "add " + resource); } } } } private void transferDeleteAndRefresh(final List<EbookPropertyItem> ebooks) { final MainController controller = MainController.getController(); for(final EbookPropertyItem item : ebooks) { //do not process items younger than 10 seconds since jeboorker has touched them. if(isTimeLeft(item, 10000)) { final IResourceHandler resourceHandler = item.getResourceHandler(); if(!resourceHandler.exists()) { //remove SwingUtilities.invokeLater(new Runnable() { @Override public void run() { boolean removed = controller.removeEbookPropertyItem(item); controller.getMainTreeHandler().refreshFileSystemTreeEntry(item.getResourceHandler()); if(!removed) { DefaultDBManager.getInstance().deleteObject(item); } LoggerFactory.getLogger().log(Level.INFO, "remove " + resourceHandler + " " + removed); } }); } else { //refresh EbookPropertyItemUtils.refreshEbookPropertyItem(item, resourceHandler, true); DefaultDBManager.getInstance().updateObject(item); LoggerFactory.getLogger().log(Level.INFO, "refresh " + resourceHandler); } FileRefreshBackground.getInstance().addEbooks(ebooks); } } } private boolean isTimeLeft(EbookPropertyItem item, long time) { long timestamp = item.getTimestamp(); if(System.currentTimeMillis() - timestamp < time) { return false; } return true; } } }