blob: 3e62baef90b491aa80018a77f92c064da2074b8c [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.configuration2;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.configuration2.event.ConfigurationEvent;
import org.apache.commons.configuration2.event.EventListener;
import org.apache.commons.configuration2.event.EventSource;
import org.apache.commons.configuration2.event.EventType;
import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
import org.apache.commons.configuration2.sync.LockMode;
import org.apache.commons.configuration2.tree.DefaultConfigurationKey;
import org.apache.commons.configuration2.tree.DefaultExpressionEngine;
import org.apache.commons.configuration2.tree.ExpressionEngine;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.commons.configuration2.tree.NodeCombiner;
import org.apache.commons.configuration2.tree.NodeTreeWalker;
import org.apache.commons.configuration2.tree.QueryResult;
import org.apache.commons.configuration2.tree.TreeUtils;
import org.apache.commons.configuration2.tree.UnionCombiner;
/**
* <p>
* A hierarchical composite configuration class.
* </p>
* <p>
* This class maintains a list of configuration objects, which can be added
* using the diverse {@code addConfiguration()} 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
* {@link org.apache.commons.configuration2.tree.NodeCombiner NodeCombiner}
* 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}, this may be a complex operation.
* </p>
* <p>
* Because of the way a {@code CombinedConfiguration} 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()}, or {@code clearProperty()} on a
* {@code CombinedConfiguration} is not strictly forbidden, however, depending
* on the current {@link NodeCombiner} and the involved properties, the results
* may be different than expected. Some examples may illustrate this:
* </p>
* <ul>
* <li>Imagine a {@code CombinedConfiguration} <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} a
* {@link org.apache.commons.configuration2.tree.OverrideCombiner
* OverrideCombiner} is used. This combiner will ensure that defined user
* settings take precedence over the default values. If the resulting
* {@code CombinedConfiguration} is queried for the background color,
* {@code blue} will be returned because this value is defined in
* {@code user.properties}. Now consider what happens if the key
* {@code gui.background} is removed from the {@code CombinedConfiguration}:
*
* <pre>
* cc.clearProperty(&quot;gui.background&quot;);
* </pre>
*
* Will a {@code cc.containsKey("gui.background")} now return <b>false</b>? No,
* it won't! The {@code clearProperty()} 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} to be re-constructed. This time the
* {@code OverrideCombiner} cannot find a {@code gui.background} 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()} 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(&quot;database.user&quot;, &quot;scott&quot;);
* </pre>
*
* would cause such a hanging property. If now one of the child configurations
* is changed and the {@code CombinedConfiguration} 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");} will alter the second child
* configuration - because the prefix <em>home</em> is here already present;
* when the {@code CombinedConfiguration} is re-constructed, this change is
* taken into account.)</li>
* </ul>
* <p>
* 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} becomes
* invalid (either because one of the contained configurations was modified or
* because the {@code invalidate()} 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>
* Notes about thread-safety: This configuration implementation uses a
* {@code Synchronizer} object to protect instances against concurrent access.
* The concrete {@code Synchronizer} implementation used determines whether an
* instance of this class is thread-safe or not. In contrast to other
* implementations derived from {@link BaseHierarchicalConfiguration},
* thread-safety is an issue here because the nodes structure used by this
* configuration has to be constructed dynamically when a child configuration is
* changed. Therefore, when multiple threads are involved which also manipulate
* one of the child configurations, a proper {@code Synchronizer} object should
* be set. Note that the {@code Synchronizer} objects used by the child
* configurations do not really matter. Because immutable in-memory nodes
* structures are used for them there is no danger that updates on child
* configurations could interfere with read operations on the combined
* configuration.
* </p>
*
* @since 1.3
* @version $Id$
*/
public class CombinedConfiguration extends BaseHierarchicalConfiguration implements
EventListener<ConfigurationEvent>, Cloneable
{
/**
* Constant for the event type fired when the internal node structure of a
* combined configuration becomes invalid.
*
* @since 2.0
*/
public static final EventType<ConfigurationEvent> COMBINED_INVALIDATE =
new EventType<ConfigurationEvent>(ConfigurationEvent.ANY,
"COMBINED_INVALIDATE");
/** Constant for the expression engine for parsing the at path. */
private static final DefaultExpressionEngine AT_ENGINE = DefaultExpressionEngine.INSTANCE;
/** Constant for the default node combiner. */
private static final NodeCombiner DEFAULT_COMBINER = new UnionCombiner();
/** Constant for a root node for an empty configuration. */
private static final ImmutableNode EMPTY_ROOT = new ImmutableNode.Builder()
.create();
/** Stores the combiner. */
private NodeCombiner nodeCombiner;
/** Stores a list with the contained configurations. */
private List<ConfigData> configurations;
/** Stores a map with the named configurations. */
private Map<String, Configuration> namedConfigurations;
/**
* An expression engine used for converting child configurations to
* hierarchical ones.
*/
private ExpressionEngine conversionExpressionEngine;
/** A flag whether this configuration is up-to-date. */
private boolean upToDate;
/**
* Creates a new instance of {@code CombinedConfiguration} 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)
{
nodeCombiner = (comb != null) ? comb : DEFAULT_COMBINER;
initChildCollections();
}
/**
* Creates a new instance of {@code CombinedConfiguration} that uses
* a union combiner.
*
* @see org.apache.commons.configuration2.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()
{
beginRead(true);
try
{
return nodeCombiner;
}
finally
{
endRead();
}
}
/**
* 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} 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!");
}
beginWrite(true);
try
{
this.nodeCombiner = nodeCombiner;
invalidateInternal();
}
finally
{
endWrite();
}
}
/**
* Returns the {@code ExpressionEngine} for converting flat child
* configurations to hierarchical ones.
*
* @return the conversion expression engine
* @since 1.6
*/
public ExpressionEngine getConversionExpressionEngine()
{
beginRead(true);
try
{
return conversionExpressionEngine;
}
finally
{
endRead();
}
}
/**
* Sets the {@code ExpressionEngine} for converting flat child
* configurations to hierarchical ones. When constructing the root node for
* this combined configuration the properties of all child configurations
* must be combined to a single hierarchical node structure. In this
* process, non hierarchical configurations are converted to hierarchical
* ones first. This can be problematic if a child configuration contains
* keys that are no compatible with the default expression engine used by
* hierarchical configurations. Therefore it is possible to specify a
* specific expression engine to be used for this purpose.
*
* @param conversionExpressionEngine the conversion expression engine
* @see ConfigurationUtils#convertToHierarchical(Configuration, ExpressionEngine)
* @since 1.6
*/
public void setConversionExpressionEngine(
ExpressionEngine conversionExpressionEngine)
{
beginWrite(true);
try
{
this.conversionExpressionEngine = conversionExpressionEngine;
}
finally
{
endWrite();
}
}
/**
* 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} will
* be thrown. With the optional {@code at} 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 "database.tables"},
* 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(Configuration config, String name,
String at)
{
if (config == null)
{
throw new IllegalArgumentException(
"Added configuration must not be null!");
}
beginWrite(true);
try
{
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);
if (getLogger().isDebugEnabled())
{
getLogger()
.debug("Adding configuration " + config + " with name "
+ name);
}
configurations.add(cd);
if (name != null)
{
namedConfigurations.put(name, config);
}
invalidateInternal();
}
finally
{
endWrite();
}
registerListenerAt(config);
}
/**
* 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(Configuration 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(Configuration 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()
{
beginRead(true);
try
{
return getNumberOfConfigurationsInternal();
}
finally
{
endRead();
}
}
/**
* 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)
{
beginRead(true);
try
{
ConfigData cd = configurations.get(index);
return cd.getConfiguration();
}
finally
{
endRead();
}
}
/**
* 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)
{
beginRead(true);
try
{
return namedConfigurations.get(name);
}
finally
{
endRead();
}
}
/**
* Returns a List of all the configurations that have been added.
* @return A List of all the configurations.
* @since 1.7
*/
public List<Configuration> getConfigurations()
{
beginRead(true);
try
{
List<Configuration> list =
new ArrayList<Configuration>(getNumberOfConfigurationsInternal());
for (ConfigData cd : configurations)
{
list.add(cd.getConfiguration());
}
return list;
}
finally
{
endRead();
}
}
/**
* Returns a List of the names of all the configurations that have been
* added in the order they were added. A NULL value will be present in
* the list for each configuration that was added without a name.
* @return A List of all the configuration names.
* @since 1.7
*/
public List<String> getConfigurationNameList()
{
beginRead(true);
try
{
List<String> list = new ArrayList<String>(getNumberOfConfigurationsInternal());
for (ConfigData cd : configurations)
{
list.add(cd.getName());
}
return list;
}
finally
{
endRead();
}
}
/**
* 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 (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 = configurations.remove(index);
if (cd.getName() != null)
{
namedConfigurations.remove(cd.getName());
}
unregisterListenerAt(cd.getConfiguration());
invalidateInternal();
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<String> getConfigurationNames()
{
beginRead(true);
try
{
return namedConfigurations.keySet();
}
finally
{
endRead();
}
}
/**
* 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} 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()
{
beginWrite(true);
try
{
invalidateInternal();
}
finally
{
endWrite();
}
}
/**
* 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
*/
@Override
public void onEvent(ConfigurationEvent event)
{
if (event.isBeforeUpdate())
{
invalidate();
}
}
/**
* Clears this configuration. All contained configurations will be removed.
*/
@Override
protected void clearInternal()
{
unregisterListenerAtChildren();
initChildCollections();
invalidateInternal();
}
/**
* 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
*/
@Override
public Object clone()
{
beginRead(false);
try
{
CombinedConfiguration copy = (CombinedConfiguration) super.clone();
copy.initChildCollections();
for (ConfigData cd : configurations)
{
copy.addConfiguration(ConfigurationUtils.cloneConfiguration(cd
.getConfiguration()), cd.getName(), cd.getAt());
}
return copy;
}
finally
{
endRead();
}
}
/**
* 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} 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!");
}
Set<Configuration> sources = getSources(key);
if (sources.isEmpty())
{
return null;
}
Iterator<Configuration> iterator = sources.iterator();
Configuration source = iterator.next();
if (iterator.hasNext())
{
throw new IllegalArgumentException("The key " + key
+ " is defined by multiple sources!");
}
return source;
}
/**
* Returns a set with the configuration sources, in which the specified key
* is defined. This method determines the configuration nodes that are
* identified by the given key. It then determines the configuration sources
* to which these nodes belong and adds them to the result set. Note the
* following points:
* <ul>
* <li>If no node object is found for this key, an empty set is 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 combined configuration is contained in the result
* set.</li>
* </ul>
*
* @param key the key of a configuration property
* @return a set with the configuration sources, which contain this property
* @since 2.0
*/
public Set<Configuration> getSources(String key)
{
beginRead(false);
try
{
List<QueryResult<ImmutableNode>> results = fetchNodeList(key);
Set<Configuration> sources = new HashSet<Configuration>();
for (QueryResult<ImmutableNode> result : results)
{
Set<Configuration> resultSources =
findSourceConfigurations(result.getNode());
if (resultSources.isEmpty())
{
// key must be defined in combined configuration
sources.add(this);
}
else
{
sources.addAll(resultSources);
}
}
return sources;
}
finally
{
endRead();
}
}
/**
* {@inheritDoc} This implementation checks whether a combined root node
* is available. If not, it is constructed by requesting a write lock.
*/
@Override
protected void beginRead(boolean optimize)
{
if (optimize)
{
// just need a lock, don't construct configuration
super.beginRead(true);
return;
}
boolean lockObtained = false;
do
{
super.beginRead(false);
if (isUpToDate())
{
lockObtained = true;
}
else
{
// release read lock and try to obtain a write lock
endRead();
beginWrite(false); // this constructs the root node
endWrite();
}
} while (!lockObtained);
}
/**
* {@inheritDoc} This implementation checks whether a combined root node
* is available. If not, it is constructed now.
*/
@Override
protected void beginWrite(boolean optimize)
{
super.beginWrite(true);
if (optimize)
{
// just need a lock, don't construct configuration
return;
}
try
{
if (!isUpToDate())
{
getSubConfigurationParentModel().replaceRoot(
constructCombinedNode(), this);
upToDate = true;
}
}
catch (RuntimeException rex)
{
endWrite();
throw rex;
}
}
/**
* Returns a flag whether this configuration has been invalidated. This
* means that the combined nodes structure has to be rebuilt before the
* configuration can be accessed.
*
* @return a flag whether this configuration is invalid
*/
private boolean isUpToDate()
{
return upToDate;
}
/**
* Marks this configuration as invalid. This means that the next access
* re-creates the root node. An invalidate event is also fired. Note:
* This implementation expects that an exclusive (write) lock is held on
* this instance.
*/
private void invalidateInternal()
{
upToDate = false;
fireEvent(COMBINED_INVALIDATE, null, null, false);
}
/**
* Initializes internal data structures for storing information about
* child configurations.
*/
private void initChildCollections()
{
configurations = new ArrayList<ConfigData>();
namedConfigurations = new HashMap<String, Configuration>();
}
/**
* Creates the root node of this combined configuration.
*
* @return the combined root node
*/
private ImmutableNode constructCombinedNode()
{
if (getNumberOfConfigurationsInternal() < 1)
{
if (getLogger().isDebugEnabled())
{
getLogger().debug("No configurations defined for " + this);
}
return EMPTY_ROOT;
}
else
{
Iterator<ConfigData> it = configurations.iterator();
ImmutableNode node = it.next().getTransformedRoot();
while (it.hasNext())
{
node = nodeCombiner.combine(node,
it.next().getTransformedRoot());
}
if (getLogger().isDebugEnabled())
{
ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintStream stream = new PrintStream(os);
TreeUtils.printTree(stream, node);
getLogger().debug(os.toString());
}
return node;
}
}
/**
* Determines the configurations to which the specified node belongs. This
* is done by inspecting the nodes structures of all child configurations.
*
* @param node the node
* @return a set with the owning configurations
*/
private Set<Configuration> findSourceConfigurations(ImmutableNode node)
{
Set<Configuration> result = new HashSet<Configuration>();
FindNodeVisitor<ImmutableNode> visitor =
new FindNodeVisitor<ImmutableNode>(node);
for (ConfigData cd : configurations)
{
NodeTreeWalker.INSTANCE.walkBFS(cd.getRootNode(), visitor,
getModel().getNodeHandler());
if (visitor.isFound())
{
result.add(cd.getConfiguration());
visitor.reset();
}
}
return result;
}
/**
* Registers this combined configuration as listener at the given child
* configuration.
*
* @param configuration the child configuration
*/
private void registerListenerAt(Configuration configuration)
{
if (configuration instanceof EventSource)
{
((EventSource) configuration).addEventListener(
ConfigurationEvent.ANY, this);
}
}
/**
* Removes this combined configuration as listener from the given child
* configuration.
*
* @param configuration the child configuration
*/
private void unregisterListenerAt(Configuration configuration)
{
if (configuration instanceof EventSource)
{
((EventSource) configuration).removeEventListener(
ConfigurationEvent.ANY, this);
}
}
/**
* Removes this combined configuration as listener from all child
* configurations. This method is called on a clear() operation.
*/
private void unregisterListenerAtChildren()
{
if (configurations != null)
{
for (ConfigData child : configurations)
{
unregisterListenerAt(child.getConfiguration());
}
}
}
/**
* Returns the number of child configurations in this combined
* configuration. The internal list of child configurations is accessed
* without synchronization.
*
* @return the number of child configurations
*/
private int getNumberOfConfigurationsInternal()
{
return configurations.size();
}
/**
* An internal helper class for storing information about contained
* configurations.
*/
private class ConfigData
{
/** Stores a reference to the configuration. */
private final Configuration configuration;
/** Stores the name under which the configuration is stored. */
private final String name;
/** Stores the at information as path of nodes. */
private final Collection<String> atPath;
/** Stores the at string.*/
private final String at;
/** Stores the root node for this child configuration.*/
private ImmutableNode rootNode;
/**
* Creates a new instance of {@code ConfigData} and initializes
* it.
*
* @param config the configuration
* @param n the name
* @param at the at position
*/
public ConfigData(Configuration config, String n, String at)
{
configuration = config;
name = n;
atPath = parseAt(at);
this.at = at;
}
/**
* Returns the stored configuration.
*
* @return the configuration
*/
public Configuration 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 ImmutableNode 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 ImmutableNode getTransformedRoot()
{
ImmutableNode configRoot = getRootNodeOfConfiguration();
return (atPath == null) ? configRoot : prependAtPath(configRoot);
}
/**
* Prepends the at path to the given node.
*
* @param node the root node of the represented configuration
* @return the new root node including the at path
*/
private ImmutableNode prependAtPath(ImmutableNode node)
{
ImmutableNode.Builder pathBuilder = new ImmutableNode.Builder();
Iterator<String> pathIterator = atPath.iterator();
prependAtPathComponent(pathBuilder, pathIterator.next(),
pathIterator, node);
return new ImmutableNode.Builder(1).addChild(pathBuilder.create())
.create();
}
/**
* Handles a single component of the at path. A corresponding node is
* created and added to the hierarchical path to the original root node
* of the configuration.
*
* @param builder the current node builder object
* @param currentComponent the name of the current path component
* @param components an iterator with all components of the at path
* @param orgRoot the original root node of the wrapped configuration
*/
private void prependAtPathComponent(ImmutableNode.Builder builder,
String currentComponent, Iterator<String> components,
ImmutableNode orgRoot)
{
builder.name(currentComponent);
if (components.hasNext())
{
ImmutableNode.Builder childBuilder =
new ImmutableNode.Builder();
prependAtPathComponent(childBuilder, components.next(),
components, orgRoot);
builder.addChild(childBuilder.create());
}
else
{
builder.addChildren(orgRoot.getChildren());
builder.addAttributes(orgRoot.getAttributes());
builder.value(orgRoot.getValue());
}
}
/**
* Obtains the root node of the wrapped configuration. If necessary, a
* hierarchical representation of the configuration has to be created
* first.
*
* @return the root node of the associated configuration
*/
private ImmutableNode getRootNodeOfConfiguration()
{
getConfiguration().lock(LockMode.READ);
try
{
ImmutableNode root =
ConfigurationUtils
.convertToHierarchical(getConfiguration(),
conversionExpressionEngine).getNodeModel()
.getInMemoryRepresentation();
rootNode = root;
return root;
}
finally
{
getConfiguration().unlock(LockMode.READ);
}
}
/**
* Splits the at path into its components.
*
* @param at the at string
* @return a collection with the names of the single components
*/
private Collection<String> parseAt(String at)
{
if (at == null)
{
return null;
}
Collection<String> result = new ArrayList<String>();
DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(
AT_ENGINE, at).iterator();
while (it.hasNext())
{
result.add(it.nextKey());
}
return result;
}
}
}