package com.jfilter.components; import com.jfilter.FileWatcherEvent; import org.springframework.beans.factory.DisposableBean; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Controller; import java.io.File; import java.io.IOException; import java.nio.file.ClosedWatchServiceException; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.HashMap; import java.util.Map; import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; /** * File watcher class * * <p>This class used for watching on file modified event * Also class uses Spring Scheduling mechanism for periodically checking files */ @EnableScheduling @Controller public class FileWatcher implements DisposableBean { private static final Long FILE_MODIFY_THRESHOLD = 1000L; private static final String FILE_MODIFY_DELAY = "2000"; private WatchService watcher; private Map<WatchKey, Path> watchKeys; private Map<File, FileRecord> fileRecords; private boolean closed; private boolean overflowed; /** * File watcher record */ public static class FileRecord { private final File file; private final FileWatcherEvent event; private Long lastModified; /** * Creates a new instance of the {@link FileRecord} class. * * @param file file * @param event event which occurs on file modification */ public FileRecord(File file, FileWatcherEvent event) { this.file = file; this.event = event; this.lastModified = file.lastModified(); } public Long getLastModified() { return lastModified; } public void setLastModified(Long lastModified) { this.lastModified = lastModified; } /** * On file modify occur * * @return {@link Boolean} true if event not null, otherwise false */ public boolean onEvent() { if (event != null) { event.onEvent(file); return true; } else return false; } } /** * Creates a new instance of the {@link FileWatcher} class. * * @throws IOException If an I/O error occurs */ public FileWatcher() throws IOException { closed = false; overflowed = false; watcher = FileSystems.getDefault().newWatchService(); watchKeys = new HashMap<>(); fileRecords = new HashMap<>(); } /** * Add new file watcher * * @param file file * @param event event which occurs on file modification * @return {@link Boolean} true if watcher is added, otherwise false */ public boolean add(File file, FileWatcherEvent event) { if (file == null || !file.exists()) return false; fileRecords.put(file, new FileRecord(file, event)); try { Path path = file.isDirectory() ? Paths.get(file.getPath()) : Paths.get(file.getParent()); if (!watchKeys.containsValue(path)) { watchKeys.put(path.register(watcher, ENTRY_MODIFY), path); return true; } else return true; } catch (IOException e) { return false; } } /** * Check if file is modified * * <p>Method compares file lastModified value and lastModified which added on watcher creation * If difference between this two values is greater than FILE_MODIFY_THRESHOLD then method returns true, * otherwise false * * <p>FILE_MODIFY_THRESHOLD used because when file is modifying it modifies twice: * <ul> * <li>When file content modified</li> * <li>When file modification information changed</li> * </ul> * Therefore we need to filter this duplicated modify events. * * @param file file * @return true if file modified, otherwise false */ public boolean fileIsModified(File file) { boolean result = false; if (fileRecords.containsKey(file)) { FileRecord record = fileRecords.get(file); Long lastModified = record.getLastModified(); if (file.lastModified() - lastModified > FILE_MODIFY_THRESHOLD) { record.setLastModified(file.lastModified()); result = true; } } return result; } /** * Process all modify events * * @throws InterruptedException if interrupted while waiting */ @SuppressWarnings("unchecked") private void processModifiedFiles() throws InterruptedException { WatchKey key = watcher.take(); for (WatchEvent<?> event : key.pollEvents()) { WatchEvent.Kind<?> kind = event.kind(); if (kind == StandardWatchEventKinds.OVERFLOW) { overflowed = true; continue; } if (watchKeys.containsKey(key)) { WatchEvent<Path> ev = (WatchEvent<Path>) event; String filename = String.format("%s%s%s", watchKeys.get(key).toString(), File.separator, ev.context().toString()); File file = new File(filename); if (fileIsModified(file)) fileRecords.get(file).onEvent(); } } key.reset(); } /** * Process modify events by schedule * * <p>FILE_MODIFY_DELAY used for set schedule repeat delay */ @Scheduled(fixedDelayString = FILE_MODIFY_DELAY) public void scheduleModifiedFiles() { try { if (!closed) processModifiedFiles(); } catch (ClosedWatchServiceException e) { closed = true; } catch (InterruptedException e) { closed = true; Thread.currentThread().interrupt(); } } public boolean isClosed() { return closed; } public void setClosed(boolean closed) { this.closed = closed; } public WatchService getWatcher() { return watcher; } /** * Destroying watcher * * @throws IOException if this watch service is closed */ public void destroy() throws IOException { if (!closed) { watcher.close(); closed = true; } } public boolean isOverflowed() { return overflowed; } }