/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.model.core.internal.folder; import static java.nio.file.StandardWatchEventKinds.*; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.nio.file.Path; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.eclipse.smarthome.config.core.ConfigConstants; import org.eclipse.smarthome.core.service.AbstractWatchService; import org.eclipse.smarthome.model.core.ModelParser; import org.eclipse.smarthome.model.core.ModelRepository; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; /** * This class is able to observe multiple folders for changes and notifies the * model repository about every change, so that it can update itself. * * @author Kai Kreuzer - Initial contribution and API * @author Fabio Marini - Refactoring to use WatchService * @author Ana Dimova - reduce to a single watch thread for all class instances * */ public class FolderObserver extends AbstractWatchService implements ManagedService { public FolderObserver() { super(ConfigConstants.getConfigFolder()); } /* the model repository is provided as a service */ private ModelRepository modelRepo = null; /* map that stores a list of valid file extensions for each folder */ private final Map<String, String[]> folderFileExtMap = new ConcurrentHashMap<String, String[]>(); /* set of file extensions for which we have parsers already registered */ private static Set<String> parsers = new HashSet<>(); /* set of files that have been ignored due to a missing parser */ private static Set<File> ignoredFiles = new HashSet<>(); public void setModelRepository(ModelRepository modelRepo) { this.modelRepo = modelRepo; } public void unsetModelRepository(ModelRepository modelRepo) { this.modelRepo = null; } protected void addModelParser(ModelParser modelParser) { parsers.add(modelParser.getExtension()); processIgnoredFiles(modelParser.getExtension()); } protected void removeModelParser(ModelParser modelParser) { parsers.remove(modelParser.getExtension()); } @Override public void activate() { } private void processIgnoredFiles(String extension) { HashSet<File> clonedSet = new HashSet<>(ignoredFiles); for (File file : clonedSet) { if (extension.equals(getExtension(file.getPath()))) { checkFile(modelRepo, file, ENTRY_CREATE); ignoredFiles.remove(file); } } } @Override protected boolean watchSubDirectories() { return true; } @Override protected Kind<?>[] getWatchEventKinds(Path directory) { if (directory != null && MapUtils.isNotEmpty(folderFileExtMap)) { String folderName = directory.getFileName().toString(); if (folderFileExtMap.containsKey(folderName)) { return new Kind<?>[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; } } return null; } @Override @SuppressWarnings("rawtypes") public synchronized void updated(Dictionary config) throws ConfigurationException { if (config != null) { // necessary to check removed models Map<String, String[]> previousFolderFileExtMap = new ConcurrentHashMap<String, String[]>(folderFileExtMap); // make sure to clear the caches first folderFileExtMap.clear(); Enumeration keys = config.keys(); while (keys.hasMoreElements()) { String foldername = (String) keys.nextElement(); if (foldername.equals("service.pid")) { continue; } String[] fileExts = ((String) config.get(foldername)).split(","); File folder = getFile(foldername); if (folder.exists() && folder.isDirectory()) { folderFileExtMap.put(foldername, fileExts); } else { logger.warn("Directory '{}' does not exist in '{}'. Please check your configuration settings!", foldername, ConfigConstants.getConfigFolder()); } } notifyUpdateToModelRepo(previousFolderFileExtMap); deactivate(); super.activate(); } } private void notifyUpdateToModelRepo(Map<String, String[]> previousFolderFileExtMap) { checkDeletedModels(previousFolderFileExtMap); if (MapUtils.isNotEmpty(folderFileExtMap)) { Iterator<String> iterator = folderFileExtMap.keySet().iterator(); while (iterator.hasNext()) { String folderName = iterator.next(); final String[] validExtension = folderFileExtMap.get(folderName); if (validExtension != null && validExtension.length > 0) { File folder = getFile(folderName); File[] files = folder.listFiles(new FileExtensionsFilter(validExtension)); if (files != null && files.length > 0) { for (File file : files) { // we omit parsing of hidden files possibly created by editors or operating systems if (!file.isHidden()) { checkFile(modelRepo, file, ENTRY_CREATE); } } } } } } } private void checkDeletedModels(Map<String, String[]> previousFolderFileExtMap) { if (MapUtils.isNotEmpty(previousFolderFileExtMap)) { List<String> modelsToRemove = new LinkedList<String>(); if (MapUtils.isNotEmpty(folderFileExtMap)) { Set<String> folders = previousFolderFileExtMap.keySet(); for (String folder : folders) { if (!folderFileExtMap.containsKey(folder)) { Iterable<String> models = modelRepo.getAllModelNamesOfType(folder); if (models != null) { modelsToRemove.addAll(Lists.newLinkedList(models)); } } } } else { Set<String> folders = previousFolderFileExtMap.keySet(); for (String folder : folders) { synchronized (FolderObserver.class) { Iterable<String> models = modelRepo.getAllModelNamesOfType(folder); if (models != null) { modelsToRemove.addAll(Lists.newLinkedList(models)); } } } } if (CollectionUtils.isNotEmpty(modelsToRemove)) { for (String modelToRemove : modelsToRemove) { synchronized (FolderObserver.class) { modelRepo.removeModel(modelToRemove); } } } } } protected class FileExtensionsFilter implements FilenameFilter { private String[] validExtensions; public FileExtensionsFilter(String[] validExtensions) { this.validExtensions = validExtensions; } @Override public boolean accept(File dir, String name) { if (validExtensions != null && validExtensions.length > 0) { for (String extension : validExtensions) { if (name.toLowerCase().endsWith("." + extension)) { return true; } } } return false; } } @SuppressWarnings("rawtypes") private static void checkFile(final ModelRepository modelRepo, final File file, final Kind kind) { if (modelRepo != null && file != null) { try { synchronized (FolderObserver.class) { if ((kind == ENTRY_CREATE || kind == ENTRY_MODIFY)) { if (parsers.contains(getExtension(file.getName()))) { try (FileInputStream inputStream = FileUtils.openInputStream(file)) { modelRepo.addOrRefreshModel(file.getName(), inputStream); } catch (IOException e) { LoggerFactory.getLogger(FolderObserver.class) .warn("Error while opening file during update: {}", file.getAbsolutePath()); } } else { ignoredFiles.add(file); } } else if (kind == ENTRY_DELETE) { modelRepo.removeModel(file.getName()); } } } catch (Exception e) { LoggerFactory.getLogger(FolderObserver.class).error("Error handling update of file '{}': {}.", file.getAbsolutePath(), e.getMessage(), e); } } } private static File getFileByFileExtMap(Map<String, String[]> folderFileExtMap, String filename) { if (StringUtils.isNotBlank(filename) && MapUtils.isNotEmpty(folderFileExtMap)) { String extension = getExtension(filename); if (StringUtils.isNotBlank(extension)) { Set<Entry<String, String[]>> entries = folderFileExtMap.entrySet(); Iterator<Entry<String, String[]>> iterator = entries.iterator(); while (iterator.hasNext()) { Entry<String, String[]> entry = iterator.next(); if (ArrayUtils.contains(entry.getValue(), extension)) { return new File(getFile(entry.getKey()) + File.separator + filename); } } } } return null; } /** * Returns the {@link File} object for the given filename. <br /> * It must be contained in the configuration folder * * @param filename * the file name to get the {@link File} for * @return the corresponding {@link File} */ private static File getFile(String filename) { File folder = new File(ConfigConstants.getConfigFolder() + File.separator + filename); return folder; } /** * Returns the extension of the given file * * @param filename * the file name to get the extension * @return the file's extension */ public static String getExtension(String filename) { String fileExt = filename.substring(filename.lastIndexOf(".") + 1); return fileExt; } @Override protected void processWatchEvent(WatchEvent<?> event, Kind<?> kind, Path path) { File toCheck = getFileByFileExtMap(folderFileExtMap, path.getFileName().toString()); if (toCheck != null && !toCheck.isHidden()) { checkFile(modelRepo, toCheck, kind); } } }