package org.eclipse.smarthome.automation.module.script.rulesupport.internal.loader; import static java.nio.file.StandardWatchEventKinds.*; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Path; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.util.Comparator; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.eclipse.smarthome.automation.module.script.ScriptEngineContainer; import org.eclipse.smarthome.automation.module.script.ScriptEngineManager; import org.eclipse.smarthome.config.core.ConfigConstants; import org.eclipse.smarthome.core.service.AbstractWatchService; /** * The {@link ScriptFileWatcher} watches the jsr223 directory for files. If a new/modified file is detected, the script * is read and passed to the {@link ScriptEngineManager}. * * @author Simon Merschjohann - initial contribution * @author Kai Kreuzer - improved logging and removed thread pool * */ public class ScriptFileWatcher extends AbstractWatchService { private static final String FILE_DIRECTORY = "automation" + File.separator + "jsr223"; private static final long INITIAL_DELAY = 25; private static final long RECHECK_INTERVAL = 20; private long earliestStart = System.currentTimeMillis() + INITIAL_DELAY * 1000; private ScriptEngineManager manager; private Map<String, Set<URL>> urlsByScriptExtension = new ConcurrentHashMap<>(); private Set<URL> loaded = new HashSet<>(); public ScriptFileWatcher() { super(ConfigConstants.getConfigFolder() + File.separator + FILE_DIRECTORY); } public void setScriptEngineManager(ScriptEngineManager manager) { this.manager = manager; } @Override public void activate() { super.activate(); importResources(new File(pathToWatch)); startScheduler(); } /** * Imports resources from the specified file or directory. * * @param file the file or directory to import resources from */ private void importResources(File file) { if (file.exists()) { File[] files = file.listFiles(); if (files != null) { for (File f : files) { if (!f.isHidden()) { importResources(f); } } } else { try { URL url = file.toURI().toURL(); importFile(url); } catch (MalformedURLException e) { // can't happen for the 'file' protocol handler with a correctly formatted URI logger.debug("Can't create a URL", e); } } } } @Override protected boolean watchSubDirectories() { return true; } @Override protected Kind<?>[] getWatchEventKinds(Path subDir) { return new Kind<?>[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; } @Override protected void processWatchEvent(WatchEvent<?> event, Kind<?> kind, Path path) { File file = path.toFile(); if (!file.isHidden()) { try { URL fileUrl = file.toURI().toURL(); if (kind.equals(ENTRY_DELETE)) { this.removeFile(fileUrl); } if (file.canRead() && (kind.equals(ENTRY_CREATE) || kind.equals(ENTRY_MODIFY))) { this.importFile(fileUrl); } } catch (MalformedURLException e) { logger.error("malformed", e); } } } private void removeFile(URL url) { dequeueUrl(url); manager.removeEngine(getScriptIdentifier(url)); loaded.remove(url); } private synchronized void importFile(URL url) { String fileName = getFileName(url); if (loaded.contains(url)) { this.removeFile(url); // if already loaded, remove first } String scriptType = getScriptType(url); if (scriptType != null) { if (System.currentTimeMillis() < earliestStart) { enqueueUrl(url, scriptType); } else { if (manager.isSupported(scriptType)) { try (InputStreamReader reader = new InputStreamReader(new BufferedInputStream(url.openStream()))) { logger.info("Loading script '{}'", fileName); ScriptEngineContainer container = manager.createScriptEngine(scriptType, fileName); if (container != null) { manager.loadScript(container.getIdentifier(), reader); loaded.add(url); logger.debug("Script loaded: {}", fileName); } else { logger.error("Script loading error, ignoring file: {}", fileName); } } catch (IOException e) { logger.error("Failed to load file '{}': {}", url.getFile(), e.getMessage()); } } else { enqueueUrl(url, scriptType); logger.info("ScriptEngine for {} not available", scriptType); } } } } private String getFileName(URL url) { String fileName = url.getFile(); String parentPath = FILE_DIRECTORY.replace('\\', '/'); if (fileName.contains(parentPath)) { fileName = fileName.substring(fileName.lastIndexOf(parentPath) + parentPath.length() + 1); } return fileName; } private void enqueueUrl(URL url, String scriptType) { synchronized (urlsByScriptExtension) { Set<URL> set = urlsByScriptExtension.get(scriptType); if (set == null) { set = new HashSet<URL>(); urlsByScriptExtension.put(scriptType, set); } set.add(url); logger.debug("in queue: {}", urlsByScriptExtension); } } private void dequeueUrl(URL url) { String scriptType = getScriptType(url); if (scriptType != null) { synchronized (urlsByScriptExtension) { Set<URL> set = urlsByScriptExtension.get(scriptType); if (set != null) { set.remove(url); if (set.isEmpty()) { urlsByScriptExtension.remove(scriptType); } } logger.debug("in queue: {}", urlsByScriptExtension); } } } private String getScriptType(URL url) { String fileName = url.getPath(); int idx = fileName.lastIndexOf("."); if (idx == -1) { return null; } String fileExtension = fileName.substring(idx + 1); // ignore known file extensions for "temp" files if (fileExtension.equals("txt") || fileExtension.endsWith("~") || fileExtension.endsWith("swp")) { return null; } return fileExtension; } private String getScriptIdentifier(URL url) { return url.toString(); } private void startScheduler() { ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleWithFixedDelay(this::checkFiles, INITIAL_DELAY, RECHECK_INTERVAL, TimeUnit.SECONDS); } private void checkFiles() { SortedSet<URL> reimportUrls = new TreeSet<URL>(new Comparator<URL>() { @Override public int compare(URL o1, URL o2) { String f1 = o1.getPath(); String s1 = f1.substring(f1.lastIndexOf("/") + 1); String f2 = o2.getPath(); String s2 = f2.substring(f2.lastIndexOf("/") + 1); return String.CASE_INSENSITIVE_ORDER.compare(s1, s2); } }); synchronized (urlsByScriptExtension) { HashSet<String> newlySupported = new HashSet<>(); for (String key : urlsByScriptExtension.keySet()) { if (manager.isSupported(key)) { newlySupported.add(key); } } for (String key : newlySupported) { reimportUrls.addAll(urlsByScriptExtension.remove(key)); } } for (URL url : reimportUrls) { importFile(url); } } }