/* * Copyright 2008-2014 the original author or authors * * 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.kaleidofoundry.core.config; import static org.kaleidofoundry.core.config.ConfigurationConstants.KeyPropertiesRoot; import static org.kaleidofoundry.core.config.ConfigurationConstants.KeyPropertiesSeparator; import static org.kaleidofoundry.core.config.ConfigurationConstants.KeyRoot; import static org.kaleidofoundry.core.config.ConfigurationConstants.KeySeparator; import static org.kaleidofoundry.core.config.ConfigurationContextBuilder.CacheManagerRef; import static org.kaleidofoundry.core.config.ConfigurationContextBuilder.FileStoreRef; import static org.kaleidofoundry.core.config.ConfigurationContextBuilder.FileStoreUri; import static org.kaleidofoundry.core.config.ConfigurationContextBuilder.Name; import static org.kaleidofoundry.core.config.ConfigurationContextBuilder.StorageAllowed; import static org.kaleidofoundry.core.config.ConfigurationContextBuilder.UpdateAllowed; import static org.kaleidofoundry.core.i18n.InternalBundleHelper.ConfigurationMessageBundle; import java.io.Serializable; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.LinkedBlockingQueue; import javax.swing.event.EventListenerList; import org.kaleidofoundry.core.cache.Cache; import org.kaleidofoundry.core.cache.CacheManager; import org.kaleidofoundry.core.cache.CacheManagerFactory; import org.kaleidofoundry.core.config.ConfigurationChangeEvent.ConfigurationChangeType; import org.kaleidofoundry.core.config.model.FireChangesReport; import org.kaleidofoundry.core.context.EmptyContextParameterException; import org.kaleidofoundry.core.context.RuntimeContext; import org.kaleidofoundry.core.lang.annotation.Immutable; import org.kaleidofoundry.core.lang.annotation.NotNull; import org.kaleidofoundry.core.lang.annotation.Task; import org.kaleidofoundry.core.lang.annotation.TaskLabel; import org.kaleidofoundry.core.lang.annotation.ThreadSafe; import org.kaleidofoundry.core.store.FileStore; import org.kaleidofoundry.core.store.FileStoreContextBuilder; import org.kaleidofoundry.core.store.FileStoreFactory; import org.kaleidofoundry.core.store.ResourceException; import org.kaleidofoundry.core.store.ResourceHandler; import org.kaleidofoundry.core.store.SingleFileStore; import org.kaleidofoundry.core.util.AbstractPropertyAccessor; import org.kaleidofoundry.core.util.CollectionsHelper; import org.kaleidofoundry.core.util.StringHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <p> * Abstract ancestor class of all implementations of Configuration, <br/> * It implements the basic and commons functionalities of the interface configuration. <br/> * It uses internally a {@link Cache} instance (thread safe) to handle key / property items<br/> * </p> * * @author jraduget */ @Immutable @ThreadSafe @Task(comment = "bootstrap load for classpath or file uri configuration which can defined a specific cacheManagerRef or resourceStorageRef, ... which is not yet loaded at build time", labels = TaskLabel.Defect) public abstract class AbstractConfiguration extends AbstractPropertyAccessor implements Configuration { protected static final Logger LOGGER = LoggerFactory.getLogger(Configuration.class); // configuration name identifier protected final String name; // internal parameters cache protected final Cache<String, Serializable> cacheProperties; // external persistent singleFileStore protected final SingleFileStore singleFileStore; // internal runtime context protected final RuntimeContext<Configuration> context; // configuration listeners support protected final EventListenerList listeners; // ordered & thread safe queue of the changes applied on the configuration properties private final LinkedBlockingQueue<ConfigurationChangeEvent> changesEvents; /** * @param context * @throws ResourceException */ protected AbstractConfiguration(@NotNull final RuntimeContext<Configuration> context) throws ResourceException { this(null, null, context); } /** * @param name * @param resourceUri * @param context * @throws ResourceException * @throws IllegalArgumentException if resourceUri is illegal ({@link URISyntaxException}) */ protected AbstractConfiguration(String name, String resourceUri, @NotNull final RuntimeContext<Configuration> context) throws ResourceException { super(context.getString(ConfigurationContextBuilder.MultiValuesSeparator), context.getString(ConfigurationContextBuilder.DateTimeFormat), context .getString(ConfigurationContextBuilder.NumberFormat)); // argument & context inputs name = !StringHelper.isEmpty(name) ? name : (!StringHelper.isEmpty(context.getString(Name)) ? context.getString(Name) : context.getName()); resourceUri = !StringHelper.isEmpty(resourceUri) ? resourceUri : context.getString(FileStoreUri); // context check if (StringHelper.isEmpty(name)) { throw new EmptyContextParameterException(Name, context); } if (StringHelper.isEmpty(resourceUri)) { throw new EmptyContextParameterException(FileStoreUri, context); } this.context = context; this.name = name; // internal file store instantiation final String fileStoreRef = context.getString(FileStoreRef, "configurations." + this.name + ".unnamed"); final FileStore fileStore = FileStoreFactory.provides(resourceUri, new FileStoreContextBuilder(fileStoreRef).build()); singleFileStore = new SingleFileStore(resourceUri, fileStore); // internal cache key / value instantiation final CacheManager cacheManager; final String cacheManagerContextRef = context.getString(CacheManagerRef); if (!StringHelper.isEmpty(cacheManagerContextRef)) { cacheManager = CacheManagerFactory.provides(new RuntimeContext<CacheManager>(cacheManagerContextRef, CacheManager.class, context)); } else { cacheManager = CacheManagerFactory.provides(); } cacheProperties = cacheManager.getCache("kaleidofoundry/configuration/" + name); // events listeners listeners = new EventListenerList(); changesEvents = new LinkedBlockingQueue<ConfigurationChangeEvent>(); } /** * don't use it, * this constructor is only needed and used by some IOC framework like spring. */ AbstractConfiguration() { name = null; listeners = null; cacheProperties = null; context = null; singleFileStore = null; changesEvents = null; } /** * you don't need to release resourceHandler argument, it is done by agregator <br/> * <b>be careful, if you use {@link #setProperty(String, Serializable)}, event will be fired...</b> a preferred way is to use * cacheProperties.put(normalizeKey(key), value) * * @param resourceHandler * @param cacheProperties * @return internal cache instance * @throws ResourceException * @throws ConfigurationException */ protected abstract Cache<String, Serializable> loadProperties(ResourceHandler resourceHandler, Cache<String, Serializable> cacheProperties) throws ResourceException, ConfigurationException; /** * you don't need to release resourceHandler argument, it is done by agregator * * @param resourceHandler * @param cacheProperties * @return internal cache instance * @throws ResourceException * @throws ConfigurationException */ protected abstract Cache<String, Serializable> storeProperties(final ResourceHandler resourceHandler, Cache<String, Serializable> cacheProperties) throws ResourceException, ConfigurationException; /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#getName() */ @Override public String getName() { return name; } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#getResourceUri() */ @Override public String getResourceUri() { return singleFileStore.getResourceBinding(); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#isStorageAllowed() */ @Override public boolean isStorable() { if (StringHelper.isEmpty(context.getString(StorageAllowed))) { return true; } else { return Boolean.valueOf(context.getString(StorageAllowed)); } } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#isUpdateAllowed() */ @Override public boolean isUpdateable() { if (StringHelper.isEmpty(context.getString(UpdateAllowed))) { return true; } else { return Boolean.valueOf(context.getString(UpdateAllowed)); } } // ************************************************************************** // -> Load / singleFileStore management // ************************************************************************** /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#isLoaded() */ @Override public boolean isLoaded() { return singleFileStore.isLoaded(); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#load() */ @Override public final synchronized void load() throws ResourceException, ConfigurationException { if (isLoaded()) { throw new ConfigurationException("config.load.already", name); } LOGGER.info(ConfigurationMessageBundle.getMessage("config.load.info", name, getResourceUri())); final ResourceHandler resourceHandler = singleFileStore.get(); try { loadProperties(resourceHandler, cacheProperties); } finally { resourceHandler.close(); } } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#store() */ @Override public final synchronized void store() throws ResourceException { if (!isLoaded()) { throw new ConfigurationException("config.load.notloaded", name); } if (!isStorable()) { throw new ConfigurationException("config.readonly.store", name); } LOGGER.info(ConfigurationMessageBundle.getMessage("config.save.info", name, getResourceUri())); final ResourceHandler resourceHandler = singleFileStore.get(); try { storeProperties(resourceHandler, cacheProperties); } finally { resourceHandler.close(); } } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#unload() */ @Override public final synchronized void unload() throws ResourceException, ConfigurationException { if (!isLoaded()) { throw new ConfigurationException("config.load.notloaded", name); } LOGGER.info(ConfigurationMessageBundle.getMessage("config.unload.info", name, getResourceUri())); // cleanup cache entries cacheProperties.clear(); // unload store singleFileStore.unload(); // fire unload event fireUnload(); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#reload() */ @Override public final synchronized void reload() throws ResourceException, ConfigurationException { if (!isLoaded()) { throw new ConfigurationException("config.load.notloaded", name); } // backup old entries final Map<String, Serializable> oldItems = new HashMap<String, Serializable>(); for (final String oldPropName : cacheProperties.keys()) { oldItems.put(oldPropName, cacheProperties.get(oldPropName)); } LOGGER.info(ConfigurationMessageBundle.getMessage("config.unload.info", name, getResourceUri())); // cleanup cache entries cacheProperties.clear(); // unload store singleFileStore.unload(); // load it load(); // compare each property - if there are different : fire appropriate event for (final Entry<String, Serializable> entry : oldItems.entrySet()) { // a property have been removed if (!cacheProperties.keys().contains(entry.getKey())) { firePropertyRemove(entry.getKey(), entry.getValue()); continue; } // a property have been changed final Serializable oldValue = oldItems.get(entry.getKey()); final Serializable newValue = cacheProperties.get(entry.getKey()); if (oldValue != null && oldValue.equals(newValue)) { firePropertyUpdate(entry.getKey(), entry.getValue(), cacheProperties.get(entry.getKey())); continue; } } } // *************************************************************************** // -> listener used for configuration changes management // *************************************************************************** /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#addConfigurationChangeListener(java.beans.PropertyChangeListener) */ @Override public void addConfigurationListener(final ConfigurationListener listener) { listeners.add(ConfigurationListener.class, listener); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#removeConfigurationChangeListener(java.beans.PropertyChangeListener) */ @Override public void removeConfigurationListener(final ConfigurationListener listener) { listeners.remove(ConfigurationListener.class, listener); } /** * fire event will be queued until {@link #fireConfigurationChangesEvents} was called * * @param propertyName * @param newValue */ protected void firePropertyCreate(final String propertyName, final Serializable newValue) { final ConfigurationChangeEvent event = ConfigurationChangeEvent.newCreateEvent(this, propertyName, newValue); changesEvents.add(event); } /** * fire event will be queued until {@link #fireConfigurationChangesEvents} was called * * @param propertyName * @param oldValue * @param newValue */ protected void firePropertyUpdate(final String propertyName, final Serializable oldValue, final Serializable newValue) { final ConfigurationChangeEvent event = ConfigurationChangeEvent.newUpdateEvent(this, propertyName, oldValue, newValue); changesEvents.add(event); } /** * fire event will be queued until {@link #fireConfigurationChangesEvents} was called * * @param propertyName * @param oldValue */ protected void firePropertyRemove(final String propertyName, final Serializable oldValue) { final ConfigurationChangeEvent event = ConfigurationChangeEvent.newRemoveEvent(this, propertyName, oldValue); changesEvents.add(event); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#fireConfigurationChangesEvents() */ @Override public FireChangesReport fireConfigurationChangesEvents() { int created = 0; int updated = 0; int removed = 0; int listenerCount = 0; // count event by types for (final ConfigurationChangeEvent event : changesEvents) { if (event.getConfigurationChangeType() == ConfigurationChangeType.CREATE) { created++; } if (event.getConfigurationChangeType() == ConfigurationChangeType.UPDATE) { updated++; } if (event.getConfigurationChangeType() == ConfigurationChangeType.REMOVE) { removed++; } } // fire event to registered listener for (final ConfigurationListener listener : listeners.getListeners(ConfigurationListener.class)) { for (final ConfigurationChangeEvent event : changesEvents) { if (event.getConfigurationChangeType() == ConfigurationChangeType.CREATE) { listener.propertyCreate(event); } if (event.getConfigurationChangeType() == ConfigurationChangeType.UPDATE) { listener.propertyUpdate(event); } if (event.getConfigurationChangeType() == ConfigurationChangeType.REMOVE) { listener.propertyRemove(event); } listenerCount++; } } // clear past fire events changesEvents.clear(); // created message report FireChangesReport fireReport = new FireChangesReport(getName(), singleFileStore.getResourceBinding(), created, updated, removed, listenerCount); // log message LOGGER.info(ConfigurationMessageBundle.getMessage("config.firechanges.info0", name, singleFileStore.getResourceBinding(), fireReport.getCreated(), fireReport.getUpdated(), fireReport.getRemoved(), fireReport.getListernerCount())); LOGGER.info(ConfigurationMessageBundle.getMessage("config.firechanges.info1", fireReport.getCreated(), fireReport.getUpdated(), fireReport.getRemoved())); LOGGER.info(ConfigurationMessageBundle.getMessage("config.firechanges.info2", fireReport.getListernerCount())); return fireReport; } /** */ protected void fireUnload() { for (final ConfigurationListener listener : listeners.getListeners(ConfigurationListener.class)) { listener.configurationUnload(this); } } // *************************************************************************** // -> Roots management // *************************************************************************** /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#getRoots() */ @Override public Set<String> roots() { return roots(KeyRoot); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#getRoots(java.lang.String) */ @Override public Set<String> roots(final String prefix) { final String fullKey = normalizeKey(prefix); final Set<String> roots = new LinkedHashSet<String>(); for (String pKey : cacheProperties.keys()) { if (pKey.startsWith(fullKey)) { pKey = pKey.substring(fullKey.length()); final StringTokenizer st = new StringTokenizer(pKey, KeySeparator); if (st.hasMoreTokens()) { final String root = st.nextToken(); if (!roots.contains(root)) { roots.add(root); } } } } return roots; } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#containsRoot(java.lang. String, java.lang.String) */ @Override public boolean containsRoot(final String rootKey, final String prefixRoot) { return roots(prefixRoot).contains(rootKey); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#getRootsIterator() */ @Override public Iterator<String> rootsIterator() { return roots().iterator(); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#getRootsIterator(java.lang .String) */ @Override public Iterator<String> rootsIterator(final String prefix) { return roots(prefix).iterator(); } // *************************************************************************** // -> Keys management // *************************************************************************** /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#keySet() */ @Override public Set<String> keySet() { return keySet(KeyRoot); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#keySet(java.lang.String) */ @Override public Set<String> keySet(final String prefix) { final String fullKey = normalizeKey(prefix); final Set<String> keys = new LinkedHashSet<String>(); for (final String pKey : cacheProperties.keys()) { if (pKey.startsWith(fullKey)) { keys.add(pKey); } } return keys; } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#containsKey(java.lang.String) */ @Override public boolean containsKey(final String key) { return containsKey(key, ""); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.config.Configuration#containsKey(java .lang.String) */ @Override public boolean containsKey(final String key, final String prefix) { return keySet(prefix).contains(normalizeKey(key)); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.config.Configuration#getKeysIterator() */ @Override public Iterator<String> keysIterator() { return keySet().iterator(); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.config.Configuration#getKeysIterator( java.lang.String) */ @Override public Iterator<String> keysIterator(final String prefix) { return keySet(prefix).iterator(); } // *************************************************************************** // -> Property value accessors // *************************************************************************** /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#getProperty(java.lang.String) */ @Override public Serializable getProperty(final String key) { return cacheProperties.get(normalizeKey(key)); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#setProperty(java.lang.String, java.io.Serializable) */ @Override public void setProperty(@NotNull final String key, @NotNull final Serializable newValue) { if (!isUpdateable()) { throw new ConfigurationException("config.readonly.update", name); } // normalize the given key final String fullKey = normalizeKey(key); // is it a new property ? final boolean newProperty = !cacheProperties.keys().contains(fullKey); // memorize old value for fire event final Serializable oldValue = cacheProperties.get(fullKey); // update cache data cacheProperties.put(fullKey, newValue); // fire change event if (newProperty) { firePropertyCreate(fullKey, newValue); } else { firePropertyUpdate(fullKey, oldValue, newValue); } } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#removeProperty(java.lang.String) */ @Override public void removeProperty(@NotNull final String key) { if (!isUpdateable()) { throw new ConfigurationException("config.readonly.update", name); } // normalize the given key final String fullKey = normalizeKey(key); // memorize old value for fire event final Serializable oldValue = cacheProperties.get(fullKey); // remove it from cache cacheProperties.remove(fullKey); // fire change event firePropertyRemove(fullKey, oldValue); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#toProperties(java.lang.String) */ @Override public Properties toProperties(final String rootPrefix) { final Properties prop = new Properties(); final Set<String> keys = keySet(rootPrefix); for (final String string : keys) { String propPath = string; final String propValue = getString(propPath); final List<String> propValues = getStringList(propPath); propPath = StringHelper.replaceAll(propPath, KeyRoot, KeyPropertiesRoot); propPath = StringHelper.replaceAll(propPath, KeySeparator, KeyPropertiesSeparator); if (propValues != null) { prop.put(propPath, CollectionsHelper.collectionToString(propValues, MultiValuesSeparator)); } else if (propValue != null) { prop.put(propPath, propValue); } } return prop; } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#toProperties() */ @Override public Properties toProperties() { return toProperties(KeyRoot); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#addConfiguration(org.kaleidofoundry.core.config.Configuration) */ @Override public Configuration addConfiguration(final Configuration config) { if (config != null) { for (final String propName : config.keySet()) { final Serializable propValue = config.getProperty(propName); if (propValue != null) { setProperty(propName, propValue); } } } return this; } /* * (non-Javadoc) * @see org.kaleidofoundry.core.config.Configuration#extractConfiguration(java.lang.String, * org.kaleidofoundry.core.config.Configuration) */ @Override public Configuration extractConfiguration(@NotNull String prefix, @NotNull final Configuration outConfiguration) { prefix = normalizeKey(prefix); for (final String propName : keySet()) { final Serializable propValue = getProperty(propName); if (StringHelper.isEmpty(prefix)) { outConfiguration.setProperty(propName, propValue); } else { final String newPropName = StringHelper.replaceAll(propName, prefix + KeySeparator, ""); if (propValue != null && !propName.equals(newPropName)) { outConfiguration.setProperty(newPropName, propValue); } } } return outConfiguration; } /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { final StringBuilder str = new StringBuilder("{"); for (final Iterator<String> it = keysIterator(); it.hasNext();) { final String key = it.next(); final String value = getString(key); final List<String> values = getStringList(key); if (values != null && values.size() <= 1) { str.append(key).append("=").append(value).append(" , "); } else { str.append(key).append("=").append(CollectionsHelper.collectionToString(values, MultiValuesSeparator)).append(" , "); } } str.append("}"); return str.toString(); } /** * Normalize property path from argument. If standard property key is used like "application.name", it will be internally convert to * "//application/name" * * @param propertyPath * @return Normalize propertyPath argument */ public static String normalizeKey(@NotNull final String propertyPath) { final StringBuilder normalizeKey = new StringBuilder(); if (!propertyPath.startsWith(KeyRoot)) { normalizeKey.append(KeyRoot); } normalizeKey.append(StringHelper.replaceAll(propertyPath, KeyPropertiesSeparator, KeySeparator)); return normalizeKey.toString(); } }