/** * 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.config.dispatch.internal; import static java.nio.file.StandardWatchEventKinds.*; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Path; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.util.Arrays; import java.util.Comparator; import java.util.Dictionary; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.eclipse.smarthome.config.core.ConfigConstants; import org.eclipse.smarthome.core.service.AbstractWatchService; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class provides a mean to read any kind of configuration data from * config folder files and dispatch it to the different bundles using the {@link ConfigurationAdmin} service. * * <p> * The name of the configuration folder can be provided as a program argument "smarthome.configdir" (default is "conf"). * Configurations for OSGi services are kept in a subfolder that can be provided as a program argument * "smarthome.servicedir" (default is "services"). Any file in this folder with the extension .cfg will be processed. * </p> * * <p> * The format of the configuration file is similar to a standard property file, with the exception that the property * name can be prefixed by the service pid of the {@link ManagedService}: * </p> * <p> * <service-pid>:<property>=<value> * </p> * <p> * In case the pid does not contain any ".", the default service pid namespace is prefixed, which can be defined by the * program argument "smarthome.servicepid" (default is "org.eclipse.smarthome"). * </p> * <p> * If no pid is defined in the property line, the default pid namespace will be used together with the filename. E.g. if * you have a file "security.cfg", the pid that will be used is "org.eclipse.smarthome.security". * </p> * <p> * Last but not least, a pid can be defined in the first line of a cfg file by prefixing it with "pid:", e.g. * "pid: com.acme.smarthome.security". * * @author Kai Kreuzer - Initial contribution and API * @author Petar Valchev - Added sort by modification time, when configuration files are read * @author Ana Dimova - reduce to a single watch thread for all class instances */ public class ConfigDispatcher extends AbstractWatchService { private static String getPathToWatch() { String progArg = System.getProperty(SERVICEDIR_PROG_ARGUMENT); if (progArg != null) { return ConfigConstants.getConfigFolder() + File.separator + progArg; } else { return ConfigConstants.getConfigFolder() + File.separator + SERVICES_FOLDER; } } public ConfigDispatcher() { super(getPathToWatch()); } /** The program argument name for setting the service config directory path */ final static public String SERVICEDIR_PROG_ARGUMENT = "smarthome.servicedir"; /** The program argument name for setting the service pid namespace */ final static public String SERVICEPID_PROG_ARGUMENT = "smarthome.servicepid"; /** * The program argument name for setting the default services config file * name */ final static public String SERVICECFG_PROG_ARGUMENT = "smarthome.servicecfg"; /** The default folder name of the configuration folder of services */ final static public String SERVICES_FOLDER = "services"; /** The default namespace for service pids */ final static public String SERVICE_PID_NAMESPACE = "org.eclipse.smarthome"; /** The default services configuration filename */ final static public String SERVICE_CFG_FILE = "smarthome.cfg"; private static final String PID_MARKER = "pid:"; private final Logger logger = LoggerFactory.getLogger(ConfigDispatcher.class); private ConfigurationAdmin configAdmin; @Override public void activate() { super.activate(); readDefaultConfig(); readConfigs(); } @Override public void deactivate() { super.deactivate(); } protected void setConfigurationAdmin(ConfigurationAdmin configAdmin) { this.configAdmin = configAdmin; } protected void unsetConfigurationAdmin(ConfigurationAdmin configAdmin) { this.configAdmin = null; } /* * (non-Javadoc) * * @see * org.eclipse.smarthome.core.service.AbstractWatchService#watchSubDirectories * () */ @Override protected boolean watchSubDirectories() { return false; } /* * (non-Javadoc) * * @see * org.eclipse.smarthome.core.service.AbstractWatchService#registerDirectory * (java.nio.file.Path) */ @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) { if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) { try { File f = path.toFile(); if (!f.isHidden()) { processConfigFile(f); } } catch (IOException e) { logger.warn("Could not process config file '{}': {}", path, e); } } } private String getDefaultServiceConfigFile() { String progArg = System.getProperty(SERVICECFG_PROG_ARGUMENT); if (progArg != null) { return progArg; } else { return ConfigConstants.getConfigFolder() + File.separator + SERVICE_CFG_FILE; } } private void readDefaultConfig() { File defaultCfg = new File(getDefaultServiceConfigFile()); try { processConfigFile(defaultCfg); } catch (IOException e) { logger.warn("Could not process default config file '{}': {}", getDefaultServiceConfigFile(), e); } } private void readConfigs() { File dir = getSourcePath().toFile(); if (dir.exists()) { File[] files = dir.listFiles(); // Sort the files by modification time, // so that the last modified file is processed last. Arrays.sort(files, new Comparator<File>() { @Override public int compare(File left, File right) { return Long.valueOf(left.lastModified()).compareTo(right.lastModified()); } }); for (File file : files) { try { processConfigFile(file); } catch (IOException e) { logger.warn("Could not process config file '{}': {}", file.getName(), e); } } } else { logger.debug("Configuration folder '{}' does not exist.", dir.toString()); } } private static String getServicePidNamespace() { String progArg = System.getProperty(SERVICEPID_PROG_ARGUMENT); if (progArg != null) { return progArg; } else { return SERVICE_PID_NAMESPACE; } } @SuppressWarnings({ "unchecked", "rawtypes" }) private void processConfigFile(File configFile) throws IOException, FileNotFoundException { if (configFile.isDirectory() || !configFile.getName().endsWith(".cfg")) { logger.debug("Ignoring file '{}'", configFile.getName()); return; } logger.debug("Processing config file '{}'", configFile.getName()); // we need to remember which configuration needs to be updated // because values have changed. Map<Configuration, Dictionary> configsToUpdate = new HashMap<Configuration, Dictionary>(); // also cache the already retrieved configurations for each pid Map<Configuration, Dictionary> configMap = new HashMap<Configuration, Dictionary>(); String pid; String filenameWithoutExt = StringUtils.substringBeforeLast(configFile.getName(), "."); if (filenameWithoutExt.contains(".")) { // it is a fully qualified namespace pid = filenameWithoutExt; } else { pid = getServicePidNamespace() + "." + filenameWithoutExt; } // configuration file contains a PID Marker List<String> lines = IOUtils.readLines(new FileInputStream(configFile)); if (lines.size() > 0 && lines.get(0).startsWith(PID_MARKER)) { pid = lines.get(0).substring(PID_MARKER.length()).trim(); } for (String line : lines) { String[] contents = parseLine(configFile.getPath(), line); // no valid configuration line, so continue if (contents == null) { continue; } if (contents[0] != null) { pid = contents[0]; // PID is not fully qualified, so prefix with namespace if (!pid.contains(".")) { pid = getServicePidNamespace() + "." + pid; } } String property = contents[1]; String value = contents[2]; Configuration configuration = configAdmin.getConfiguration(pid, null); if (configuration != null) { Dictionary configProperties = configMap.get(configuration); if (configProperties == null) { configProperties = configuration.getProperties() != null ? configuration.getProperties() : new Properties(); configMap.put(configuration, configProperties); } if (!value.equals(configProperties.get(property))) { configProperties.put(property, value); configsToUpdate.put(configuration, configProperties); } } } for (Entry<Configuration, Dictionary> entry : configsToUpdate.entrySet()) { entry.getKey().update(entry.getValue()); } } private String[] parseLine(final String filePath, final String line) { String trimmedLine = line.trim(); if (trimmedLine.startsWith("#") || trimmedLine.isEmpty()) { return null; } String pid = null; // no override of the pid String key = StringUtils.substringBefore(trimmedLine, "="); if (key.contains(":")) { pid = StringUtils.substringBefore(key, ":"); trimmedLine = trimmedLine.substring(pid.length() + 1); pid = pid.trim(); } if (!trimmedLine.isEmpty() && trimmedLine.substring(1).contains("=")) { String property = StringUtils.substringBefore(trimmedLine, "="); String value = trimmedLine.substring(property.length() + 1); return new String[] { pid, property.trim(), value.trim() }; } else { logger.warn("Could not parse line '{}'", line); return null; } } }