blob: 4cee442d824b143946235b806ff4d6887feba7dc [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.commons.configuration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.configuration.event.ConfigurationEvent;
import org.apache.commons.configuration.event.ConfigurationListener;
import org.apache.commons.configuration.tree.ConfigurationNode;
import org.apache.commons.configuration.tree.DefaultConfigurationKey;
import org.apache.commons.configuration.tree.DefaultConfigurationNode;
import org.apache.commons.configuration.tree.DefaultExpressionEngine;
import org.apache.commons.configuration.tree.NodeCombiner;
import org.apache.commons.configuration.tree.UnionCombiner;
import org.apache.commons.configuration.tree.ViewNode;
/**
* <p>
* A hierarchical composite configuration class.
* </p>
* <p>
* This class maintains a list of configuration objects, which can be added
* using the divers <code>addConfiguration()</code> methods. After that the
* configurations can be accessed either by name (if one was provided when the
* configuration was added) or by index. For the whole set of managed
* configurations a logical node structure is constructed. For this purpose a
* <code>{@link org.apache.commons.configuration.tree.NodeCombiner NodeCombiner}</code>
* object can be set. This makes it possible to specify different algorithms for
* the combination process.
* </p>
* <p>
* The big advantage of this class is that it creates a truly hierarchical
* structure of all the properties stored in the contained configurations - even
* if some of them are no hierarchical configurations per se. So all enhanced
* features provided by a hierarchical configuration (e.g. choosing an
* expression engine) are applicable.
* </p>
* <p>
* The class works by registering itself as an event listener at all added
* configurations. So it gets notified whenever one of these configurations is
* changed and can invalidate its internal node structure. The next time a
* property is accessed the node structure will be re-constructed using the
* current state of the managed configurations. Note that, depending on the used
* <code>NodeCombiner</code>, this may be a complex operation.
* </p>
* <p>
* Because of the way a <code>CombinedConfiguration</code> is working it has
* more or less view character: it provides a logic view on the configurations
* it contains. In this constellation not all methods defined for hierarchical
* configurations - especially methods that update the stored properties - can
* be implemented in a consistent manner. Using such methods (like
* <code>addProperty()</code>, or <code>clearProperty()</code> on a
* <code>CombinedConfiguration</code> is not strictly forbidden, however,
* depending on the current <code>{@link NodeCombiner}</code> and the involved
* properties, the results may be different than expected. Some examples may
* illustrate this:
* </p>
* <p>
* <ul>
* <li>Imagine a <code>CombinedConfiguration</code> <em>cc</em> containing
* two child configurations with the following content:
* <dl>
* <dt>user.properties</dt>
* <dd>
*
* <pre>
* gui.background = blue
* gui.position = (10, 10, 400, 200)
* </pre>
*
* </dd>
* <dt>default.properties</dt>
* <dd>
*
* <pre>
* gui.background = black
* gui.foreground = white
* home.dir = /data
* </pre>
*
* </dd>
* </dl>
* As a <code>NodeCombiner</code> a
* <code>{@link org.apache.commons.configuration.tree.OverrideCombiner OverrideCombiner}</code>
* is used. This combiner will ensure that defined user settings take precedence
* over the default values. If the resulting <code>CombinedConfiguration</code>
* is queried for the background color, <code>blue</code> will be returned
* because this value is defined in <code>user.properties</code>. Now
* consider what happens if the key <code>gui.background</code> is removed
* from the <code>CombinedConfiguration</code>:
*
* <pre>cc.clearProperty("gui.background");</pre>
*
* Will a <code>cc.containsKey("gui.background")</code> now return <b>false</b>?
* No, it won't! The <code>clearProperty()</code> operation is executed on the
* node set of the combined configuration, which was constructed from the nodes
* of the two child configurations. It causes the value of the
* <em>background</em> node to be cleared, which is also part of the first
* child configuration. This modification of one of its child configurations
* causes the <code>CombinedConfiguration</code> to be re-constructed. This
* time the <code>OverrideCombiner</code> cannot find a
* <code>gui.background</code> property in the first child configuration, but
* it finds one in the second, and adds it to the resulting combined
* configuration. So the property is still present (with a different value now).</li>
* <li><code>addProperty()</code> can also be problematic: Most node
* combiners use special view nodes for linking parts of the original
* configurations' data together. If new properties are added to such a special
* node, they do not belong to any of the managed configurations and thus hang
* in the air. Using the same configurations as in the last example, the
* statement
*
* <pre>
* addProperty("database.user", "scott");
* </pre>
*
* would cause such a hanging property. If now one of the child configurations
* is changed and the <code>CombinedConfiguration</code> is re-constructed,
* this property will disappear! (Add operations are not problematic if they
* result in a child configuration being updated. For instance an
* <code>addProperty("home.url", "localhost");</code> will alter the second
* child configuration - because the prefix <em>home</em> is here already
* present; when the <code>CombinedConfiguration</code> is re-constructed,
* this change is taken into account.)</li>
* </ul>
* Because of such problems it is recommended to perform updates only on the
* managed child configurations.
* </p>
* <p>
* Whenever the node structure of a <code>CombinedConfiguration</code> becomes
* invalid (either because one of the contained configurations was modified or
* because the <code>invalidate()</code> method was directly called) an event
* is generated. So this can be detected by interested event listeners. This
* also makes it possible to add a combined configuration into another one.
* </p>
* <p>
* Implementation note: Adding and removing configurations to and from a
* combined configuration is not thread-safe. If a combined configuration is
* manipulated by multiple threads, the developer has to take care about
* properly synchronization.
* </p>
*
* @author <a
* href="http://commons.apache.org/configuration/team-list.html">Commons
* Configuration team</a>
* @since 1.3
* @version $Id$
*/
public class CombinedConfiguration extends HierarchicalConfiguration implements
ConfigurationListener, Cloneable
{
/**
* Constant for the invalidate event that is fired when the internal node
* structure becomes invalid.
*/
public static final int EVENT_COMBINED_INVALIDATE = 40;
/**
* The serial version ID.
*/
private static final long serialVersionUID = 8338574525528692307L;
/** Constant for the expression engine for parsing the at path. */
private static final DefaultExpressionEngine AT_ENGINE = new DefaultExpressionEngine();
/** Constant for the default node combiner. */
private static final NodeCombiner DEFAULT_COMBINER = new UnionCombiner();
/** Constant for the name of the property used for the reload check.*/
private static final String PROP_RELOAD_CHECK = "CombinedConfigurationReloadCheck";
/** Stores the combiner. */
private NodeCombiner nodeCombiner;
/** Stores the combined root node. */
private ConfigurationNode combinedRoot;
/** Stores a list with the contained configurations. */
private List configurations;
/** Stores a map with the named configurations. */
private Map namedConfigurations;
/** A flag whether an enhanced reload check is to be performed.*/
private boolean forceReloadCheck;
/**
* Creates a new instance of <code>CombinedConfiguration</code> and
* initializes the combiner to be used.
*
* @param comb the node combiner (can be <b>null</b>, then a union combiner
* is used as default)
*/
public CombinedConfiguration(NodeCombiner comb)
{
setNodeCombiner((comb != null) ? comb : DEFAULT_COMBINER);
clear();
}
/**
* Creates a new instance of <code>CombinedConfiguration</code> that uses
* a union combiner.
*
* @see org.apache.commons.configuration.tree.UnionCombiner
*/
public CombinedConfiguration()
{
this(null);
}
/**
* Returns the node combiner that is used for creating the combined node
* structure.
*
* @return the node combiner
*/
public NodeCombiner getNodeCombiner()
{
return nodeCombiner;
}
/**
* Sets the node combiner. This object will be used when the combined node
* structure is to be constructed. It must not be <b>null</b>, otherwise an
* <code>IllegalArgumentException</code> exception is thrown. Changing the
* node combiner causes an invalidation of this combined configuration, so
* that the new combiner immediately takes effect.
*
* @param nodeCombiner the node combiner
*/
public void setNodeCombiner(NodeCombiner nodeCombiner)
{
if (nodeCombiner == null)
{
throw new IllegalArgumentException(
"Node combiner must not be null!");
}
this.nodeCombiner = nodeCombiner;
invalidate();
}
/**
* Returns a flag whether an enhanced reload check must be performed.
*
* @return the force reload check flag
* @since 1.4
*/
public boolean isForceReloadCheck()
{
return forceReloadCheck;
}
/**
* Sets the force reload check flag. If this flag is set, each property
* access on this configuration will cause a reload check on the contained
* configurations. This is a workaround for a problem with some reload
* implementations that only check if a reload is required when they are
* triggered. Per default this mode is disabled. If the force reload check
* flag is set to <b>true</b>, accessing properties will be less
* performant, but reloads on contained configurations will be detected.
*
* @param forceReloadCheck the value of the flag
* @since 1.4
*/
public void setForceReloadCheck(boolean forceReloadCheck)
{
this.forceReloadCheck = forceReloadCheck;
}
/**
* Adds a new configuration to this combined configuration. It is possible
* (but not mandatory) to give the new configuration a name. This name must
* be unique, otherwise a <code>ConfigurationRuntimeException</code> will
* be thrown. With the optional <code>at</code> argument you can specify
* where in the resulting node structure the content of the added
* configuration should appear. This is a string that uses dots as property
* delimiters (independent on the current expression engine). For instance
* if you pass in the string <code>&quot;database.tables&quot;</code>,
* all properties of the added configuration will occur in this branch.
*
* @param config the configuration to add (must not be <b>null</b>)
* @param name the name of this configuration (can be <b>null</b>)
* @param at the position of this configuration in the combined tree (can be
* <b>null</b>)
*/
public void addConfiguration(AbstractConfiguration config, String name,
String at)
{
if (config == null)
{
throw new IllegalArgumentException(
"Added configuration must not be null!");
}
if (name != null && namedConfigurations.containsKey(name))
{
throw new ConfigurationRuntimeException(
"A configuration with the name '"
+ name
+ "' already exists in this combined configuration!");
}
ConfigData cd = new ConfigData(config, name, at);
configurations.add(cd);
if (name != null)
{
namedConfigurations.put(name, config);
}
config.addConfigurationListener(this);
invalidate();
}
/**
* Adds a new configuration to this combined configuration with an optional
* name. The new configuration's properties will be added under the root of
* the combined node structure.
*
* @param config the configuration to add (must not be <b>null</b>)
* @param name the name of this configuration (can be <b>null</b>)
*/
public void addConfiguration(AbstractConfiguration config, String name)
{
addConfiguration(config, name, null);
}
/**
* Adds a new configuration to this combined configuration. The new
* configuration is not given a name. Its properties will be added under the
* root of the combined node structure.
*
* @param config the configuration to add (must not be <b>null</b>)
*/
public void addConfiguration(AbstractConfiguration config)
{
addConfiguration(config, null, null);
}
/**
* Returns the number of configurations that are contained in this combined
* configuration.
*
* @return the number of contained configurations
*/
public int getNumberOfConfigurations()
{
return configurations.size();
}
/**
* Returns the configuration at the specified index. The contained
* configurations are numbered in the order they were added to this combined
* configuration. The index of the first configuration is 0.
*
* @param index the index
* @return the configuration at this index
*/
public Configuration getConfiguration(int index)
{
ConfigData cd = (ConfigData) configurations.get(index);
return cd.getConfiguration();
}
/**
* Returns the configuration with the given name. This can be <b>null</b>
* if no such configuration exists.
*
* @param name the name of the configuration
* @return the configuration with this name
*/
public Configuration getConfiguration(String name)
{
return (Configuration) namedConfigurations.get(name);
}
/**
* Removes the specified configuration from this combined configuration.
*
* @param config the configuration to be removed
* @return a flag whether this configuration was found and could be removed
*/
public boolean removeConfiguration(Configuration config)
{
for (int index = 0; index < getNumberOfConfigurations(); index++)
{
if (((ConfigData) configurations.get(index)).getConfiguration() == config)
{
removeConfigurationAt(index);
return true;
}
}
return false;
}
/**
* Removes the configuration at the specified index.
*
* @param index the index
* @return the removed configuration
*/
public Configuration removeConfigurationAt(int index)
{
ConfigData cd = (ConfigData) configurations.remove(index);
if (cd.getName() != null)
{
namedConfigurations.remove(cd.getName());
}
cd.getConfiguration().removeConfigurationListener(this);
invalidate();
return cd.getConfiguration();
}
/**
* Removes the configuration with the specified name.
*
* @param name the name of the configuration to be removed
* @return the removed configuration (<b>null</b> if this configuration
* was not found)
*/
public Configuration removeConfiguration(String name)
{
Configuration conf = getConfiguration(name);
if (conf != null)
{
removeConfiguration(conf);
}
return conf;
}
/**
* Returns a set with the names of all configurations contained in this
* combined configuration. Of course here are only these configurations
* listed, for which a name was specified when they were added.
*
* @return a set with the names of the contained configurations (never
* <b>null</b>)
*/
public Set getConfigurationNames()
{
return namedConfigurations.keySet();
}
/**
* Invalidates this combined configuration. This means that the next time a
* property is accessed the combined node structure must be re-constructed.
* Invalidation of a combined configuration also means that an event of type
* <code>EVENT_COMBINED_INVALIDATE</code> is fired. Note that while other
* events most times appear twice (once before and once after an update),
* this event is only fired once (after update).
*/
public void invalidate()
{
synchronized (getNodeCombiner()) // use combiner as lock
{
combinedRoot = null;
}
fireEvent(EVENT_COMBINED_INVALIDATE, null, null, false);
}
/**
* Event listener call back for configuration update events. This method is
* called whenever one of the contained configurations was modified. It
* invalidates this combined configuration.
*
* @param event the update event
*/
public void configurationChanged(ConfigurationEvent event)
{
invalidate();
}
/**
* Returns the configuration root node of this combined configuration. This
* method will construct a combined node structure using the current node
* combiner if necessary.
*
* @return the combined root node
*/
public ConfigurationNode getRootNode()
{
synchronized (getNodeCombiner())
{
if (combinedRoot == null)
{
combinedRoot = constructCombinedNode();
}
return combinedRoot;
}
}
/**
* Clears this configuration. All contained configurations will be removed.
*/
public void clear()
{
fireEvent(EVENT_CLEAR, null, null, true);
configurations = new ArrayList();
namedConfigurations = new HashMap();
fireEvent(EVENT_CLEAR, null, null, false);
invalidate();
}
/**
* Returns a copy of this object. This implementation performs a deep clone,
* i.e. all contained configurations will be cloned, too. For this to work,
* all contained configurations must be cloneable. Registered event
* listeners won't be cloned. The clone will use the same node combiner than
* the original.
*
* @return the copied object
*/
public Object clone()
{
CombinedConfiguration copy = (CombinedConfiguration) super.clone();
copy.clear();
for (Iterator it = configurations.iterator(); it.hasNext();)
{
ConfigData cd = (ConfigData) it.next();
copy.addConfiguration((AbstractConfiguration) ConfigurationUtils
.cloneConfiguration(cd.getConfiguration()), cd.getName(),
cd.getAt());
}
copy.setRootNode(new DefaultConfigurationNode());
return copy;
}
/**
* Returns the value of the specified property. This implementation
* evaluates the <em>force reload check</em> flag. If it is set, all
* contained configurations will be triggered before the value of the
* requested property is retrieved.
*
* @param key the key of the desired property
* @return the value of this property
* @since 1.4
*/
public Object getProperty(String key)
{
if (isForceReloadCheck())
{
for (Iterator it = configurations.iterator(); it.hasNext();)
{
try
{
// simply retrieve a property; this is enough for
// triggering a reload
((ConfigData) it.next()).getConfiguration().getProperty(
PROP_RELOAD_CHECK);
}
catch (Exception ex)
{
// ignore all exceptions, e.g. missing property exceptions
;
}
}
}
return super.getProperty(key);
}
/**
* Returns the configuration source, in which the specified key is defined.
* This method will determine the configuration node that is identified by
* the given key. The following constellations are possible:
* <ul>
* <li>If no node object is found for this key, <b>null</b> is returned.</li>
* <li>If the key maps to multiple nodes belonging to different
* configuration sources, a <code>IllegalArgumentException</code> is
* thrown (in this case no unique source can be determined).</li>
* <li>If exactly one node is found for the key, the (child) configuration
* object, to which the node belongs is determined and returned.</li>
* <li>For keys that have been added directly to this combined
* configuration and that do not belong to the namespaces defined by
* existing child configurations this configuration will be returned.</li>
* </ul>
*
* @param key the key of a configuration property
* @return the configuration, to which this property belongs or <b>null</b>
* if the key cannot be resolved
* @throws IllegalArgumentException if the key maps to multiple properties
* and the source cannot be determined, or if the key is <b>null</b>
* @since 1.5
*/
public Configuration getSource(String key)
{
if (key == null)
{
throw new IllegalArgumentException("Key must not be null!");
}
List nodes = fetchNodeList(key);
if (nodes.isEmpty())
{
return null;
}
Iterator it = nodes.iterator();
Configuration source = findSourceConfiguration((ConfigurationNode) it
.next());
while (it.hasNext())
{
Configuration src = findSourceConfiguration((ConfigurationNode) it
.next());
if (src != source)
{
throw new IllegalArgumentException("The key " + key
+ " is defined by multiple sources!");
}
}
return source;
}
/**
* Creates the root node of this combined configuration.
*
* @return the combined root node
*/
private ConfigurationNode constructCombinedNode()
{
if (getNumberOfConfigurations() < 1)
{
return new ViewNode();
}
else
{
Iterator it = configurations.iterator();
ConfigurationNode node = ((ConfigData) it.next())
.getTransformedRoot();
while (it.hasNext())
{
node = getNodeCombiner().combine(node,
((ConfigData) it.next()).getTransformedRoot());
}
return node;
}
}
/**
* Determines the configuration that owns the specified node.
*
* @param node the node
* @return the owning configuration
*/
private Configuration findSourceConfiguration(ConfigurationNode node)
{
ConfigurationNode root = null;
ConfigurationNode current = node;
// find the root node in this hierarchy
while (current != null)
{
root = current;
current = current.getParentNode();
}
// Check with the root nodes of the child configurations
for (Iterator it = configurations.iterator(); it.hasNext();)
{
ConfigData cd = (ConfigData) it.next();
if (root == cd.getRootNode())
{
return cd.getConfiguration();
}
}
return this;
}
/**
* An internal helper class for storing information about contained
* configurations.
*/
static class ConfigData
{
/** Stores a reference to the configuration. */
private AbstractConfiguration configuration;
/** Stores the name under which the configuration is stored. */
private String name;
/** Stores the at information as path of nodes. */
private Collection atPath;
/** Stores the at string.*/
private String at;
/** Stores the root node for this child configuration.*/
private ConfigurationNode rootNode;
/**
* Creates a new instance of <code>ConfigData</code> and initializes
* it.
*
* @param config the configuration
* @param n the name
* @param at the at position
*/
public ConfigData(AbstractConfiguration config, String n, String at)
{
configuration = config;
name = n;
atPath = parseAt(at);
this.at = at;
}
/**
* Returns the stored configuration.
*
* @return the configuration
*/
public AbstractConfiguration getConfiguration()
{
return configuration;
}
/**
* Returns the configuration's name.
*
* @return the name
*/
public String getName()
{
return name;
}
/**
* Returns the at position of this configuration.
*
* @return the at position
*/
public String getAt()
{
return at;
}
/**
* Returns the root node for this child configuration.
*
* @return the root node of this child configuration
* @since 1.5
*/
public ConfigurationNode getRootNode()
{
return rootNode;
}
/**
* Returns the transformed root node of the stored configuration. The
* term &quot;transformed&quot; means that an eventually defined at path
* has been applied.
*
* @return the transformed root node
*/
public ConfigurationNode getTransformedRoot()
{
ViewNode result = new ViewNode();
ViewNode atParent = result;
if (atPath != null)
{
// Build the complete path
for (Iterator it = atPath.iterator(); it.hasNext();)
{
ViewNode node = new ViewNode();
node.setName((String) it.next());
atParent.addChild(node);
atParent = node;
}
}
// Copy data of the root node to the new path
HierarchicalConfiguration hc = ConfigurationUtils
.convertToHierarchical(getConfiguration());
atParent.appendChildren(hc.getRootNode());
atParent.appendAttributes(hc.getRootNode());
rootNode = hc.getRootNode();
return result;
}
/**
* Splits the at path into its components.
*
* @param at the at string
* @return a collection with the names of the single components
*/
private Collection parseAt(String at)
{
if (at == null)
{
return null;
}
Collection result = new ArrayList();
DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(
AT_ENGINE, at).iterator();
while (it.hasNext())
{
result.add(it.nextKey());
}
return result;
}
}
}