/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.component.integration.internal; 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.ClosedWatchServiceException; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.nio.file.attribute.BasicFileAttributes; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.jackson.map.ObjectMapper; import de.rcenvironment.core.component.integration.ToolIntegrationConstants; import de.rcenvironment.core.component.integration.ToolIntegrationContext; import de.rcenvironment.core.component.integration.ToolIntegrationService; import de.rcenvironment.core.utils.common.JsonUtils; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; /** * Implementation for a file watcher in tool integration. * * @author Sascha Zur */ public class ToolIntegrationFileWatcher implements Runnable { private static final int MAX_RETRIES_REGISTER_ON_CREATE = 5; private static final int MAX_RETRIES_INTEGRATE_NEW_FILE = 5; private static final int SLEEPING_TIME = 50; private static final Log LOGGER = LogFactory.getLog(ToolIntegrationFileWatcher.class); private WatchService watcher; private ToolIntegrationContext context; private ToolIntegrationService integrationService; private Map<WatchKey, Path> registeredKeys; private AtomicBoolean isActive = new AtomicBoolean(true); private Map<Path, Long> lastModified; private Path rootContextPath; private ObjectMapper mapper = JsonUtils.getDefaultObjectMapper(); private CountDownLatch stoppingLatch; public ToolIntegrationFileWatcher(ToolIntegrationContext context, ToolIntegrationService integrationService) throws IOException { this.watcher = FileSystems.getDefault().newWatchService(); this.context = context; this.integrationService = integrationService; this.registeredKeys = new HashMap<>(); this.lastModified = new HashMap<>(); this.rootContextPath = FileSystems.getDefault().getPath(context.getRootPathToToolIntegrationDirectory(), context.getNameOfToolIntegrationDirectory()); } /** * Register a path and if it is a directory, do it recursively. * * @param path to register * @throws IOException if registration fails. */ public void registerRecursive(Path path) throws IOException { Files.walkFileTree(path, new SimpleFileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { register(dir); return FileVisitResult.CONTINUE; } }); } /** * Register given path. * * @param dir to register * @throws IOException if it fails. */ public void register(Path dir) throws IOException { if (!registeredKeys.containsValue(dir)) { WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); registeredKeys.put(key, dir); } } /** * Remove a registered path and all sub pathes. * * @param path to unregister. * @throws IOException if s.th. goes wrong */ public void unregisterRecursive(Path path) throws IOException { List<WatchKey> remove = new LinkedList<>(); for (WatchKey key : registeredKeys.keySet()) { if (registeredKeys.get(key).startsWith(path)) { remove.add(key); key.cancel(); } } for (WatchKey key : remove) { registeredKeys.remove(key); } } /** * Set watcher active/inactive. * * @param value true, if it should be active, else false. */ public void setWatcherActive(boolean value) { isActive.set(value); } /** * Stops the watcher and ends the thread. */ public void stop() { stoppingLatch = new CountDownLatch(1); try { watcher.close(); stoppingLatch.await(5, TimeUnit.SECONDS); } catch (IOException | InterruptedException e) { LOGGER.error("Error stopping watcher thread:", e); } } @SuppressWarnings("unchecked") @Override @TaskDescription("Filewatcher for integration files") public void run() { boolean running = true; while (running) { WatchKey key = null; try { if (watcher != null) { key = watcher.take(); } else { running = false; } } catch (InterruptedException e) { LOGGER.error("Got interrupted waiting for watch keys.", e); return; } catch (ClosedWatchServiceException e) { running = false; LOGGER.debug("Shut down watcher for context " + context.getContextType()); continue; } if (key == null) { LOGGER.debug("Got null WatchKey for FileWatcher of type " + context.getContextType()); continue; } Path directory = registeredKeys.get(key); if (directory == null) { LOGGER.debug(StringUtils.format("Got unregistered WatchKey for FileWatcher of type %s", context.getContextType())); continue; } for (WatchEvent<?> event : key.pollEvents()) { WatchEvent.Kind<?> kind = event.kind(); WatchEvent<Path> ev = (WatchEvent<Path>) event; Path name = ev.context(); Path child = directory.resolve(name); if (kind == StandardWatchEventKinds.OVERFLOW) { continue; } LOGGER.debug(StringUtils.format("Got event %s in context %s for file: %s", kind.name(), context.getContextType(), child.toString())); if (kind == ENTRY_CREATE) { handleCreate(child, directory); } else if (kind == ENTRY_DELETE) { handleDelete(child, directory); } else if (kind == ENTRY_MODIFY) { handleModify(child, directory); } } key.reset(); } stoppingLatch.countDown(); } private void handleCreate(Path child, Path directory) { boolean registered = false; int attempt = 0; while (!registered && attempt < MAX_RETRIES_REGISTER_ON_CREATE) { try { registerRecursive(child); registered = true; } catch (IOException x) { registered = false; LOGGER.error(StringUtils.format( "Could not register new path (Tried %s of %s times): %s; Cause: %s", ++attempt, MAX_RETRIES_REGISTER_ON_CREATE, child.toString(), x)); try { Thread.sleep(SLEEPING_TIME); } catch (InterruptedException e1) { LOGGER.error("Integration watcher sleep interrupted.", e1); } } } if (attempt == MAX_RETRIES_REGISTER_ON_CREATE) { LOGGER.error( StringUtils.format("Could not register new path after %s tries: %s", MAX_RETRIES_REGISTER_ON_CREATE, child.toString())); } if (isActive.get()) { if (Files.isDirectory(child)) { if (child.getNameCount() == rootContextPath.getNameCount() + 1) { File configurationFile = new File(child.toFile(), context.getConfigurationFilename()); integrateFile(configurationFile); } } else if (Files.isRegularFile(child)) { if (child.endsWith(ToolIntegrationConstants.PUBLISHED_COMPONENTS_FILENAME)) { integrationService.updatePublishedComponents(context); } else if (child.endsWith(context.getConfigurationFilename())) { integrateFile(child.toFile()); } else if ((child.getName(child.getNameCount() - 2).endsWith(ToolIntegrationConstants.DOCS_DIR_NAME))) { File configurationFile = new File(child.getParent().getParent().toFile(), context.getConfigurationFilename()); removeAndReintegrate(child.getParent().getParent().toFile(), configurationFile); } } } else { LOGGER.debug("Did not handle create because watcher is inactive."); } } private void handleDelete(Path child, Path directory) { try { unregisterRecursive(child); } catch (IOException x) { LOGGER.debug("Could not unregister path: " + child.toString(), x); } if (isActive.get()) { if (rootContextPath.equals(directory)) { if (child.endsWith(ToolIntegrationConstants.PUBLISHED_COMPONENTS_FILENAME)) { integrationService.updatePublishedComponents(context); } else if (child.getNameCount() == rootContextPath.getNameCount() + 1) { removeTool(child.toFile()); } } else if (child.getName(child.getNameCount() - 1).endsWith(ToolIntegrationConstants.DOCS_DIR_NAME)) { File configurationFile = new File(child.getParent().toFile(), context.getConfigurationFilename()); removeAndReintegrate(child.getParent().toFile(), configurationFile); } else if ((child.getName(child.getNameCount() - 2).endsWith(ToolIntegrationConstants.DOCS_DIR_NAME))) { File configurationFile = new File(child.getParent().getParent().toFile(), context.getConfigurationFilename()); removeAndReintegrate(child.getParent().getParent().toFile(), configurationFile); } } else { LOGGER.debug("Did not handle delete because watcher is inactive."); } } private void handleModify(Path child, Path directory) { if (isActive.get()) { final int waitingTime = 200; long currentTime = System.currentTimeMillis(); if (lastModified.get(child) == null) { modify(child, directory); } else { // Since the file watcher throws two modify events for changed content and changed // timestop, one must be ignored if (currentTime - lastModified.get(child).longValue() > waitingTime) { modify(child, directory); } else { LOGGER.debug("Skipped modify event because of too frequent calls for " + child.getFileName()); } } lastModified.put(child, currentTime); } } private void modify(Path child, Path directory) { try { final int sleepTime = 150; Thread.sleep(sleepTime); } catch (InterruptedException e) { LOGGER.debug("Sleeping for modify event interrupted"); } if (child.endsWith(ToolIntegrationConstants.PUBLISHED_COMPONENTS_FILENAME)) { integrationService.updatePublishedComponents(context); } if (directory.getNameCount() == rootContextPath.getNameCount() + 1) { File configurationFile = new File(directory.toFile(), context.getConfigurationFilename()); removeAndReintegrate(directory.toFile(), configurationFile); } if (directory.getName(directory.getNameCount() - 1).endsWith(ToolIntegrationConstants.DOCS_DIR_NAME)) { File configurationFile = new File(directory.getParent().toFile(), context.getConfigurationFilename()); removeAndReintegrate(directory.getParent().toFile(), configurationFile); } } private void removeAndReintegrate(File toRemove, File toIntegrate) { LOGGER.debug("Reload tool configuration for " + toRemove.getName()); removeTool(toRemove); integrateFile(toIntegrate); } private void removeTool(File toolDir) { String toolName = integrationService.getToolNameToPath(toolDir.getAbsolutePath()); if (toolName != null) { integrationService.removeTool(toolName, context); } } private void integrateFile(File newConfiguration) { boolean read = false; int attempt = 0; while (!read && attempt < MAX_RETRIES_INTEGRATE_NEW_FILE) { try { if (newConfiguration.exists() && newConfiguration.getAbsolutePath().endsWith(".json")) { @SuppressWarnings("unchecked") Map<String, Object> configuration = mapper.readValue(newConfiguration, new HashMap<String, Object>().getClass()); integrationService.integrateTool(configuration, context); integrationService.putToolNameToPath((String) configuration.get(ToolIntegrationConstants.KEY_TOOL_NAME), newConfiguration.getParentFile()); read = true; } else { LOGGER.debug(StringUtils.format("Configuration file does not exist or is no json file: %s", newConfiguration.getAbsolutePath())); read = true; // cancel while } } catch (IOException e) { read = false; LOGGER.error( StringUtils.format("Could not read tool configuration (Tried %s of %s times)", ++attempt, MAX_RETRIES_INTEGRATE_NEW_FILE), e); try { Thread.sleep(SLEEPING_TIME); } catch (InterruptedException e1) { LOGGER.error("Integration watcher sleep interrupted."); } } } if (attempt == MAX_RETRIES_INTEGRATE_NEW_FILE) { LOGGER.error(StringUtils.format("Could not read tool configuration after %s times. Path: %s", MAX_RETRIES_INTEGRATE_NEW_FILE, newConfiguration.getAbsolutePath())); } } public Map<WatchKey, Path> getRegisteredPaths() { return registeredKeys; } }