/* * Copyright 2014 University of Southern California * * Licensed 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 edu.usc.pgroup.floe.filesys; import edu.usc.pgroup.floe.config.ConfigProperties; import edu.usc.pgroup.floe.config.FloeConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.file.DirectoryIteratorException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Cache and listener for directory and its contents. * @author kumbhareMap */ public class DirectoryCache { /** * Logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(DirectoryCache.class); /** * The path of the directory being watched. */ private final Path watchedDirectory; /** * Flag to indicate whether or not read and cache file contents as well. * This is fine for small files, since it will read the entire file * whenever there is an update to the file. Not recommended for * potentially large files. */ private final boolean isCacheFileContents; /** * Cached data map (from base file name to FileInfo object). */ private Map<String, FileInfo> cachedData; /** * List of update listeners. * Will be notified when any of the directory events such as CreateFile, * RemoveFile, UpdateFile happens. */ private List<DirectoryUpdateListener> updateListeners; /** * The monitor thread. */ private Thread monitorThread; /** * Contructor. * @param directory the directory to cache and watch. * @param cacheFileContents if true, the contents of the files are also * cached. * TODO: Recursive watch is not supported yet. */ public DirectoryCache(final Path directory, final boolean cacheFileContents) { this.watchedDirectory = directory; this.isCacheFileContents = cacheFileContents; this.updateListeners = new ArrayList<DirectoryUpdateListener>(); this.cachedData = new HashMap<String, FileInfo>(); } /** * Get the current cached data. * @return A collection of cached data items. */ public final Collection<FileInfo> getCurrentCachedData() { return cachedData.values(); } /** * Gets the FilInfo cached data associated with the given file. * @param fileName Name of a file in the monitored directory. * @return FileInfo object containing metadata and data (if * cacheFileContents = true) */ public final FileInfo getCurrentCachedData(final String fileName) { return cachedData.get(fileName); } /** * Adds a directory update listener. * @param listener Implementation of the DirectoryUpdateListener Interface. */ public final void addListener(final DirectoryUpdateListener listener) { updateListeners.add(listener); } /** * Start monitoring the directory. * @throws java.io.IOException Throws an IOException if the directory * path is not found. */ public final void start() throws IOException { if (monitorThread == null) { monitorThread = new Thread( new DirectoryCacheMonitor(watchedDirectory)); } if (!monitorThread.isAlive()) { monitorThread.start(); } } /** * Stop monitoring the directory. */ public final void stop() { if (monitorThread != null && monitorThread.isAlive()) { monitorThread.interrupt(); } } /** * Internal class to run the directory monitoring thread. */ class DirectoryCacheMonitor implements Runnable { /** * Directory watcher service. */ private final WatchService watcher; /** * The watch key corresponding to the watched directory. */ private WatchKey watchKey; /** * Constructor. * @param dir Directory path being watched. * @throws IOException Throws an IOException if the directory * path is not found. */ DirectoryCacheMonitor(final Path dir) throws IOException { this.watcher = dir.getFileSystem().newWatchService(); LOGGER.info("Monitoring Directory: " + dir.toString()); watchKey = dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); } /** * The Monitor thread's run method. * This will execute the event processing loop. */ @Override public void run() { try (DirectoryStream<Path> stream = Files.newDirectoryStream( watchedDirectory)) { for (Path entry: stream) { String absoluteName = watchedDirectory .toAbsolutePath().toString() + FloeConfig.getConfig().getString( ConfigProperties.SYS_FILE_SEPARATOR) + entry.getFileName(); FileInfo fileInfo = new FileInfo( absoluteName); if (isCacheFileContents) { fileInfo.readFileContents(); } cachedData.put(fileInfo.getFileName(), fileInfo); LOGGER.info("File added to cachce: " + absoluteName); } for (DirectoryUpdateListener listener : updateListeners) { listener.childrenListInitialized(cachedData.values()); } } catch (DirectoryIteratorException e) { // I/O error encountered during the iteration, // the cause is an IOException LOGGER.error("Error occurred while monitoring directory. " + "No longer monitoring the directory. " + "Exception: {}", e); return; } catch (IOException e) { LOGGER.error("Error occurred while monitoring directory. " + "No longer monitoring the directory. " + "Exception: {}", e); return; } for (;;) { try { watchKey = watcher.take(); } catch (InterruptedException e) { LOGGER.error("Error occurred while monitoring directory. " + "No longer monitoring the directory. " + "Exception: {}", e); return; } for (WatchEvent<?> watchEvent: watchKey.pollEvents()) { WatchEvent.Kind<?> kind = watchEvent.kind(); if (StandardWatchEventKinds.ENTRY_CREATE == kind) { //Add info to the map. Path addedPath = ((WatchEvent<Path>) watchEvent) .context(); String absoluteName = watchedDirectory .toAbsolutePath().toString() + FloeConfig.getConfig().getString( ConfigProperties.SYS_FILE_SEPARATOR) + addedPath.toString(); FileInfo fileInfo = new FileInfo( absoluteName); if (isCacheFileContents) { fileInfo.readFileContents(); } cachedData.put(fileInfo.getFileName(), fileInfo); for (DirectoryUpdateListener listener : updateListeners) { listener.childAdded(fileInfo); } LOGGER.info("File added to cachce: " + absoluteName); } else if (StandardWatchEventKinds.ENTRY_DELETE == kind) { //Remove info from the map. Path removedPath = ((WatchEvent<Path>) watchEvent) .context(); String absoluteName = watchedDirectory .toAbsolutePath().toString() + FloeConfig.getConfig().getString( ConfigProperties.SYS_FILE_SEPARATOR) + removedPath.toString(); FileInfo fileInfo = null; if (cachedData.containsKey(absoluteName)) { fileInfo = cachedData.remove(absoluteName); } else { LOGGER.warn("The file being removed is not " + "recognized."); } for (DirectoryUpdateListener listener : updateListeners) { listener.childRemoved(fileInfo); } LOGGER.info("File removed from cache:" + absoluteName); } else if (StandardWatchEventKinds.ENTRY_MODIFY == kind) { //Update the info. //Remove info from the map. Path updatedPath = ((WatchEvent<Path>) watchEvent) .context(); String absoluteName = watchedDirectory .toAbsolutePath().toString() + FloeConfig.getConfig().getString( ConfigProperties.SYS_FILE_SEPARATOR) + updatedPath.toString(); FileInfo fileInfo = null; if (cachedData.containsKey(absoluteName)) { fileInfo = cachedData.get(absoluteName); } else { LOGGER.warn("The file being removed is not " + "recognized."); } if (fileInfo != null) { //update the info here. if (isCacheFileContents) { fileInfo.readFileContents(); } } for (DirectoryUpdateListener listener : updateListeners) { listener.childUpdated(fileInfo); } LOGGER.info("File updated in cache:" + absoluteName); } } if (!watchKey.reset()) { LOGGER.error("Could not reset the directory watch. No " + "longer monitoring the directory."); return; } } } } }