/* * Copyright (C) 2011 Rhegium Team * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.rhegium.internal.config; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.nio.file.ClosedWatchServiceException; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.rhegium.api.AbstractService; import org.rhegium.api.config.Configuration; import org.rhegium.api.config.ConfigurationProvisionException; import org.rhegium.api.config.ConfigurationService; import org.rhegium.api.config.TokenResolverManager; import org.rhegium.api.typeconverter.TypeConverterManager; import org.rhegium.internal.utils.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; import com.google.inject.name.Named; class DefaultConfigurationService extends AbstractService implements ConfigurationService { private static final Logger LOG = LoggerFactory.getLogger(DefaultConfigurationService.class); private static final Pattern REGEX = Pattern.compile("\\$\\{([a-zA-Z0-9.]*?)\\}"); private final Map<String, String> properties = new HashMap<String, String>(); private final WatchServiceTask watchServiceTask = new WatchServiceTask(); private final WatchService watchService; private final String configurationBase; @Inject private TypeConverterManager typeConverterManager; @Inject private TokenResolverManager tokenResolverManager; @Inject DefaultConfigurationService(@Named("configurationBase") String configurationBase) { this.configurationBase = configurationBase; // Prepare Java 7 FileWatch-Service try { Path basePath = new File(".").toPath(); this.watchService = basePath.getFileSystem().newWatchService(); initializeConfiguration(); } catch (IOException e) { throw new IllegalStateException("WatchService could not be prepared", e); } } private void initializeConfiguration() { // Load all properties try { if (configurationBase == null) { throw new IllegalArgumentException("configurationBase may not be null"); } File base = new File(configurationBase); if (!base.exists()) { throw new IllegalArgumentException("configurationBase must exists"); } // Start Java7 FileWatch-Service new Thread(watchServiceTask).start(); recursiveReadProperties(base); } catch (Exception e) { throw new IllegalArgumentException("Failed to load properties files", e); } } @Override public <T extends Enum<T> & Configuration<T>, V> V getProperty(T configuration) { return getProperty(configuration, null); } @Override @SuppressWarnings("unchecked") public <T extends Enum<T> & Configuration<T>, V> V getProperty(T configuration, String expression) { final String value = getProperty0(configuration.getKey(), expression, configuration.getDefaultValue(), configuration.getType(), configuration.isMultiKey() && !StringUtils.isEmpty(expression)); try { return (V) typeConverterManager.convert(value, configuration.getType()); } catch (final ConfigurationProvisionException e) { throw new ConfigurationProvisionException(String.format("Could not provision configuration key %s", configuration), e); } } @Override public <V> V getProperty(String configurationKey, String expression, Class<V> type) { final String value = getProperty0(configurationKey, expression, null, type, !StringUtils.isEmpty(expression)); try { return (V) typeConverterManager.convert(value, type); } catch (final ConfigurationProvisionException e) { throw new ConfigurationProvisionException( String.format("Could not provision configuration key %s", configurationKey), e); } } @Override public Collection<String> getKeys() { return Collections.unmodifiableCollection(properties.keySet()); } @Override public void shutdown() { try { watchService.close(); } catch (IOException e) { e.printStackTrace(); } } @Override public String getPropertyValue(String key) { String value = properties.get(key); if (value == null) { return null; } return resolveToken(value); } private String getProperty0(String key, String expression, String defaultValue, Class<?> type, boolean multiKey) { if (multiKey) { key = key.replace(".*.", "." + expression + "."); } String value = properties.get(key); if (value != null) { return resolveToken(value); } if (defaultValue != null) { return resolveToken(defaultValue); } if (type.isPrimitive()) { if (type.equals(boolean.class)) { return "false"; } return "0"; } return ""; } private String resolveToken(final String value) { final Matcher matcher = REGEX.matcher(value); int start = 0; final StringBuilder sb = new StringBuilder(); while (matcher.find()) { final String token = matcher.group(1); final String resolved = tokenResolverManager.resolveToken(token); if (resolved != null) { sb.append(value.substring(start, matcher.start())); sb.append(resolved); start = matcher.end(); } } if (sb.length() == 0) { return value; } return sb.append(value.substring(start, value.length())).toString(); } private void recursiveReadProperties(File configuration) throws IOException { if (configuration == null) { return; } if (configuration.isDirectory()) { LOG.info(StringUtils.join(" ", "Registering directory ", configuration.getName(), " for filesystem events...")); configuration.toPath().register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY); LOG.info(StringUtils.join(" ", "Searching directory ", configuration.getName(), " for configurations...")); for (File child : configuration.listFiles()) { recursiveReadProperties(child); } } else { readProperties(configuration); } } private void readProperties(File file) throws IOException { if (file == null) { return; } if (!file.getName().toUpperCase().endsWith(".PROPERTIES")) { return; } LOG.info("Reading configurations from " + file.getName()); final Properties properties = new Properties(); properties.load(new FileReader(file)); final Set<Object> keySet = properties.keySet(); for (Object key : keySet) { final String oldValue = this.properties.get(key.toString()); final String sKey = key.toString(); final String value = properties.getProperty(sKey); if (oldValue != null) { LOG.info(StringUtils.join(" ", "Overriding old configuration value '", key.toString(), "' ==> '", oldValue, "' with '", value, "'")); } this.properties.put(sKey, value); } } private class WatchServiceTask implements Runnable { @Override public void run() { try { while (true) { try { WatchKey key = watchService.take(); for (WatchEvent<?> event : key.pollEvents()) { Path path = (Path) event.context(); if (path.getFileName().toString().toLowerCase().endsWith(".properties")) { if (event.kind().equals(StandardWatchEventKinds.ENTRY_CREATE)) { LOG.info(StringUtils.join(" ", "Found new properties file: ", path.toString())); } else if (event.kind().equals(StandardWatchEventKinds.ENTRY_MODIFY)) { LOG.info(StringUtils.join(" ", "Found modified properties file: ", path.toString())); } readProperties(path.toFile()); } } if (!key.reset()) { key.cancel(); } } catch (IOException e) { // Ignore } } } catch (ClosedWatchServiceException e) { // Ignore } catch (InterruptedException e) { // Ignore } } } }