/** * Copyright (c) 2014-2016 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.automation.internal.provider.file; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.smarthome.automation.parser.Parser; import org.eclipse.smarthome.automation.parser.ParsingException; import org.eclipse.smarthome.automation.template.Template; import org.eclipse.smarthome.automation.template.TemplateProvider; import org.eclipse.smarthome.automation.type.ModuleType; import org.eclipse.smarthome.automation.type.ModuleTypeProvider; import org.eclipse.smarthome.config.core.ConfigConstants; import org.eclipse.smarthome.core.common.registry.Provider; import org.eclipse.smarthome.core.common.registry.ProviderChangeListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is base for {@link ModuleTypeProvider} and {@link TemplateProvider}, responsible for importing the * automation objects from local file system. * <p> * It provides functionality for tracking {@link Parser} services and provides common functionality for notifying the * {@link ProviderChangeListener}s for adding, updating and removing the {@link ModuleType}s or {@link Template}s. * * @author Ana Dimova - Initial Contribution * */ public abstract class AbstractFileProvider<E> implements Provider<E> { protected static final String CONFIG_PROPERTY_ROOTS = "roots"; protected Logger logger = LoggerFactory.getLogger(this.getClass()); protected String rootSubdirectory; protected String[] configurationRoots; /** * This Map provides structure for fast access to the provided automation objects. This provides opportunity for * high performance at runtime of the system, when the Rule Engine asks for any particular object, instead of * waiting it for parsing every time. * <p> * The Map has for keys URLs of the files containing automation objects and for values - parsed objects. */ protected Map<String, E> providedObjectsHolder = new ConcurrentHashMap<String, E>(); /** * This Map provides structure for fast access to the {@link Parser}s. This provides opportunity for high * performance at runtime of the system. */ private Map<String, Parser<E>> parsers = new ConcurrentHashMap<String, Parser<E>>(); /** * This map is used for mapping the imported automation objects to the file that contains them. This provides * opportunity when an event for deletion of the file is received, how to recognize which objects are removed. */ private Map<URL, List<String>> providerPortfolio = new ConcurrentHashMap<URL, List<String>>(); /** * This Map holds URL resources that waiting for a parser to be loaded. */ private Map<String, List<URL>> urls = new ConcurrentHashMap<String, List<URL>>(); private List<ProviderChangeListener<E>> listeners = new ArrayList<ProviderChangeListener<E>>(); public AbstractFileProvider(String root) { this.rootSubdirectory = root; configurationRoots = new String[] { ConfigConstants.getConfigFolder() + File.separator + "automation" }; } public void activate(Map<String, Object> config) { modified(config); } public void deactivate() { for (String root : this.configurationRoots) { deactivateWatchService(root + File.separator + rootSubdirectory); } urls.clear(); parsers.clear(); synchronized (listeners) { listeners.clear(); } providerPortfolio.clear(); providedObjectsHolder.clear(); } public synchronized void modified(Map<String, Object> config) { String roots = (String) config.get(CONFIG_PROPERTY_ROOTS); if (roots != null) { for (String root : this.configurationRoots) { if (!roots.contains(root)) { deactivateWatchService(root + File.separator + rootSubdirectory); } } this.configurationRoots = roots.split(","); } for (int i = 0; i < this.configurationRoots.length; i++) { initializeWatchService(this.configurationRoots[i] + File.separator + rootSubdirectory); } } @Override public void addProviderChangeListener(ProviderChangeListener<E> listener) { synchronized (listeners) { listeners.add(listener); } } @Override public void removeProviderChangeListener(ProviderChangeListener<E> listener) { synchronized (listeners) { listeners.remove(listener); } } /** * Imports resources from the specified file or directory. * * @param file the file or directory to import resources from */ public 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(); String parserType = getParserType(url); importFile(parserType, 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); } } } } /** * Removes resources that were loaded from the specified file or directory when the file or directory disappears. * * @param file the file or directory to import resources from */ public void removeResources(File file) { String path = file.getAbsolutePath(); for (URL key : providerPortfolio.keySet()) { try { File f = new File(key.toURI()); if (f.getAbsolutePath().startsWith(path)) { List<String> portfolio = providerPortfolio.remove(key); removeElements(portfolio); } } catch (URISyntaxException e) { // can't happen for the 'file' protocol handler with a correctly formatted URI logger.debug("Can't create a URI", e); } } } /** * This method provides functionality for tracking {@link Parser} services. * * @param parser {@link Parser} service * @param properties */ public void addParser(Parser<E> parser, Map<String, String> properties) { String parserType = properties.get(Parser.FORMAT); parserType = parserType == null ? Parser.FORMAT_JSON : parserType; parsers.put(parserType, parser); List<URL> value = urls.get(parserType); if (value != null && !value.isEmpty()) { for (URL url : value) { importFile(parserType, url); } } } /** * This method provides functionality for tracking {@link Parser} services. * * @param parser {@link Parser} service * @param properties */ public void removeParser(Parser<E> parser, Map<String, String> properties) { String parserType = properties.get(Parser.FORMAT); parserType = parserType == null ? Parser.FORMAT_JSON : parserType; parsers.remove(parserType); } /** * This method is responsible for importing a set of Automation objects from a specified URL resource. * * @param parserType is relevant to the format that you need for conversion of the Automation objects in text. * @param url a specified URL for import. */ protected void importFile(String parserType, URL url) { Parser<E> parser = parsers.get(parserType); if (parser != null) { InputStream is = null; InputStreamReader inputStreamReader = null; try { is = url.openStream(); BufferedInputStream bis = new BufferedInputStream(is); inputStreamReader = new InputStreamReader(bis); Set<E> providedObjects = parser.parse(inputStreamReader); updateProvidedObjectsHolder(url, providedObjects); } catch (ParsingException e) { logger.debug(e.getMessage(), e); } catch (IOException e) { logger.debug(e.getMessage(), e); } finally { if (inputStreamReader != null) { try { inputStreamReader.close(); } catch (IOException e) { } } if (is != null) { try { is.close(); } catch (IOException e) { } } } } else { synchronized (urls) { List<URL> value = urls.get(parserType); if (value == null) { value = new ArrayList<URL>(); urls.put(parserType, value); } value.add(url); } logger.debug("Parser {} not available", parserType, new Exception()); } } protected void updateProvidedObjectsHolder(URL url, Set<E> providedObjects) { if (providedObjects != null && !providedObjects.isEmpty()) { List<String> uids = new ArrayList<String>(); for (E providedObject : providedObjects) { String uid = getUID(providedObject); uids.add(uid); E oldProvidedObject = providedObjectsHolder.put(uid, providedObject); notifyListeners(oldProvidedObject, providedObject); } providerPortfolio.put(url, uids); } } protected void removeElements(List<String> objectsForRemove) { if (objectsForRemove != null) { for (String removedObject : objectsForRemove) { notifyListeners(providedObjectsHolder.remove(removedObject)); } } } protected void notifyListeners(E oldElement, E newElement) { synchronized (listeners) { for (ProviderChangeListener<E> listener : listeners) { if (oldElement != null) { listener.updated(this, oldElement, newElement); } else { listener.added(this, newElement); } } } } protected void notifyListeners(E removedObject) { if (removedObject != null) { synchronized (listeners) { for (ProviderChangeListener<E> listener : listeners) { listener.removed(this, removedObject); } } } } protected abstract String getUID(E providedObject); protected abstract void initializeWatchService(String watchingDir); protected abstract void deactivateWatchService(String watchingDir); private String getParserType(URL url) { String fileName = url.getPath(); if (fileName.lastIndexOf(".") == -1) { return Parser.FORMAT_JSON; } String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1); if (fileExtension.equals("txt")) { return Parser.FORMAT_JSON; } return fileExtension; } }