package com.temenos.interaction.loader.detector;
/*
* #%L
* interaction-dynamic-loader
* %%
* Copyright (C) 2012 - 2015 Temenos Holdings N.V.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
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.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.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import com.temenos.interaction.core.loader.Action;
import com.temenos.interaction.core.loader.FileEvent;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Executes actions every time a change (creation, modification or deletion) in
* a collection of directories is detected.
*
* The implementation sets a scheduled task whenever setResources or
* setListeners is called, which executes a command (in this case
* ListenerNotificationTask) every 10 seconds (by default). ListenerNotificationTask watches
* the directories for changes and is responsible of calling the execute method
* of all listener's action with the directory that changed as parameter.
*
* @author andres
* @author trojanbug
* @author cmclopes
*/
public class DirectoryChangeActionNotifier implements DirectoryChangeDetector<Action<FileEvent<File>>> {
private static final Logger logger = LoggerFactory.getLogger(DirectoryChangeActionNotifier.class);
private Collection<? extends File> resources = new ArrayList();
private Collection<? extends Action<FileEvent<File>>> listeners = new ArrayList();
private WatchService watchService;
private ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> scheduledTask = null;
private long intervalSeconds = 10;
@Override
public void setResources(Collection<? extends File> resources) {
ArrayList<File> existingResources = new ArrayList<File>();
// temporary fix to avoid crashes when trying to watch inexisting directories
// just create them if they don't exist
for (File file : resources) {
if (!file.exists()) {
try {
FileUtils.forceMkdir(file);
existingResources.add(file);
} catch (IOException ex) {
logger.warn("Could not create configured directory to monitor.", ex);
}
} else {
existingResources.add(file);
}
}
this.resources = existingResources;
initWatchers(this.resources);
}
@Override
public void setListeners(Collection<? extends Action<FileEvent<File>>> listeners) {
if (listeners == null) {
this.listeners = new ArrayList<Action<FileEvent<File>>>();
return;
}
this.listeners = new ArrayList<Action<FileEvent<File>>>(listeners);
initWatchers(getResources());
}
public Collection<? extends File> getResources() {
return resources;
}
public Collection<? extends Action<FileEvent<File>>> getListeners() {
return listeners;
}
protected void initWatchers(Collection<? extends File> resources) {
if (scheduledTask != null) {
scheduledTask.cancel(true);
}
if (resources == null || resources.isEmpty() || getListeners() == null || getListeners().isEmpty()) {
return;
}
try {
WatchService ws = FileSystems.getDefault().newWatchService();
for (File file : resources) {
Path filePath = Paths.get(file.toURI());
filePath.register(ws, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
}
watchService = ws;
scheduledTask = executorService.scheduleWithFixedDelay(new ListenerNotificationTask(watchService, getListeners(), getIntervalSeconds() * 1000), 5, getIntervalSeconds(), TimeUnit.SECONDS);
} catch (IOException ex) {
throw new RuntimeException("Error configuring directory change listener - unexpected IOException", ex);
}
}
public long getIntervalSeconds() {
return intervalSeconds;
}
public void setIntervalSeconds(long intervalSeconds) {
this.intervalSeconds = intervalSeconds;
}
/**
* Runnable class that uses a provided WatchService on files and directories
* to execute all listener's actions for detected events.
*
* It currently ignores all events in a user-specified time interval after
* the first accepted event to prevent multiples executions of the listener's for the same user-event.
* Example: Creating a single file in a folder (on UNIX system) raises two events: the ENTRY_CREATE
* and the ENTRY_MODIFY on folder. This will be changed to a scheduled run to catch the first system event
* and after the user-specified time interval executes one single call to all the listener's.
*
* @author andres
* @author trojanbug
* @author cmclopes
*/
protected static class ListenerNotificationTask implements Runnable {
private WatchService watchService;
private Collection<? extends Action<FileEvent<File>>> listeners;
private long lastRun = 0;
private long interval = 0;
public ListenerNotificationTask(WatchService watchService, Collection<? extends Action<FileEvent<File>>> listeners, long interval) {
this.watchService = watchService;
this.listeners = listeners;
this.interval = interval;
}
@Override
public void run() {
try {
WatchKey key = watchService.take();
for (WatchEvent<?> e : key.pollEvents()) {
// TODO change this for a scheduled run in the future
if (System.currentTimeMillis() - lastRun > interval) {
WatchEvent.Kind<?> kind = e.kind();
if (kind != StandardWatchEventKinds.OVERFLOW) {
Path dir = (Path) key.watchable();
Path fullPath = dir.resolve((Path) e.context());
logger.debug("Detected change ({}) in watched directory: {}", kind, fullPath);
FileEvent<File> newEvent = new DirectoryChangeEvent(fullPath.toFile());
for (Action<FileEvent<File>> action : listeners) {
logger.trace("Notifying {} about the change in {}", action, fullPath);
action.execute(newEvent);
}
}
lastRun = System.currentTimeMillis();
}
}
key.reset();
} catch (InterruptedException ex) {
}
}
}
/**
* Helper class for getting a directory from a File instance.
*
* @author andres
* @author trojanbug
* @author cmclopes
*/
public static class DirectoryChangeEvent implements FileEvent<File> {
private File directory;
public DirectoryChangeEvent(File file) {
if (!file.isDirectory()) {
directory = file.getAbsoluteFile().getParentFile();
} else {
directory = file;
}
}
@Override
public File getResource() {
return directory;
}
}
}