| /* |
| * 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 java.util.stream.Collectors; |
| |
| 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; |
| import org.apache.commons.lang3.StringUtils; |
| |
| /** |
| * <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("gui.background"); |
| * </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("database.user", "scott"); |
| * </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 |
| */ |
| public class CombinedConfiguration extends BaseHierarchicalConfiguration implements EventListener<ConfigurationEvent> { |
| /** |
| * An internal helper class for storing information about contained configurations. |
| */ |
| private final 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(final Configuration config, final String n, final String at) { |
| configuration = config; |
| name = n; |
| atPath = parseAt(at); |
| this.at = at; |
| } |
| |
| /** |
| * Gets the at position of this configuration. |
| * |
| * @return the at position |
| */ |
| public String getAt() { |
| return at; |
| } |
| |
| /** |
| * Gets the stored configuration. |
| * |
| * @return the configuration |
| */ |
| public Configuration getConfiguration() { |
| return configuration; |
| } |
| |
| /** |
| * Gets the configuration's name. |
| * |
| * @return the name |
| */ |
| public String getName() { |
| return name; |
| } |
| |
| /** |
| * Gets the root node for this child configuration. |
| * |
| * @return the root node of this child configuration |
| * @since 1.5 |
| */ |
| public ImmutableNode getRootNode() { |
| return rootNode; |
| } |
| |
| /** |
| * 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 { |
| final ImmutableNode root = ConfigurationUtils.convertToHierarchical(getConfiguration(), conversionExpressionEngine).getNodeModel() |
| .getInMemoryRepresentation(); |
| rootNode = root; |
| return root; |
| } finally { |
| getConfiguration().unlock(LockMode.READ); |
| } |
| } |
| |
| /** |
| * Gets the transformed root node of the stored configuration. The term "transformed" means that an |
| * eventually defined at path has been applied. |
| * |
| * @return the transformed root node |
| */ |
| public ImmutableNode getTransformedRoot() { |
| final ImmutableNode configRoot = getRootNodeOfConfiguration(); |
| return atPath == null ? configRoot : prependAtPath(configRoot); |
| } |
| |
| /** |
| * 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(final String at) { |
| if (StringUtils.isEmpty(at)) { |
| return null; |
| } |
| |
| final Collection<String> result = new ArrayList<>(); |
| final DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(AT_ENGINE, at).iterator(); |
| while (it.hasNext()) { |
| result.add(it.nextKey()); |
| } |
| return result; |
| } |
| |
| /** |
| * 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(final ImmutableNode node) { |
| final ImmutableNode.Builder pathBuilder = new ImmutableNode.Builder(); |
| final 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(final ImmutableNode.Builder builder, final String currentComponent, final Iterator<String> components, |
| final ImmutableNode orgRoot) { |
| builder.name(currentComponent); |
| if (components.hasNext()) { |
| final 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()); |
| } |
| } |
| } |
| |
| /** |
| * 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.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} that uses a union combiner. |
| * |
| * @see org.apache.commons.configuration2.tree.UnionCombiner |
| */ |
| public CombinedConfiguration() { |
| this(null); |
| } |
| |
| /** |
| * 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(final NodeCombiner comb) { |
| nodeCombiner = comb != null ? comb : DEFAULT_COMBINER; |
| initChildCollections(); |
| } |
| |
| /** |
| * 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(final Configuration config) { |
| addConfiguration(config, null, null); |
| } |
| |
| /** |
| * 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(final Configuration config, final String name) { |
| addConfiguration(config, name, null); |
| } |
| |
| /** |
| * 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(final Configuration config, final String name, final 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!"); |
| } |
| |
| final 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); |
| } |
| |
| /** |
| * {@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(final 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(final boolean optimize) { |
| super.beginWrite(true); |
| if (optimize) { |
| // just need a lock, don't construct configuration |
| return; |
| } |
| |
| boolean success = false; |
| try { |
| if (!isUpToDate()) { |
| getSubConfigurationParentModel().replaceRoot(constructCombinedNode(), this); |
| upToDate = true; |
| } |
| success = true; |
| } finally { |
| if (!success) { |
| endWrite(); |
| } |
| } |
| } |
| |
| /** |
| * 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 { |
| final CombinedConfiguration copy = (CombinedConfiguration) super.clone(); |
| copy.initChildCollections(); |
| configurations.forEach(cd -> copy.addConfiguration(ConfigurationUtils.cloneConfiguration(cd.getConfiguration()), cd.getName(), cd.getAt())); |
| |
| return copy; |
| } finally { |
| endRead(); |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| final Iterator<ConfigData> it = configurations.iterator(); |
| ImmutableNode node = it.next().getTransformedRoot(); |
| while (it.hasNext()) { |
| node = nodeCombiner.combine(node, it.next().getTransformedRoot()); |
| } |
| if (getLogger().isDebugEnabled()) { |
| final ByteArrayOutputStream os = new ByteArrayOutputStream(); |
| final 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(final ImmutableNode node) { |
| final Set<Configuration> result = new HashSet<>(); |
| final FindNodeVisitor<ImmutableNode> visitor = new FindNodeVisitor<>(node); |
| |
| configurations.forEach(cd -> { |
| NodeTreeWalker.INSTANCE.walkBFS(cd.getRootNode(), visitor, getModel().getNodeHandler()); |
| if (visitor.isFound()) { |
| result.add(cd.getConfiguration()); |
| visitor.reset(); |
| } |
| }); |
| |
| return result; |
| } |
| |
| /** |
| * Gets 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(final int index) { |
| beginRead(true); |
| try { |
| final ConfigData cd = configurations.get(index); |
| return cd.getConfiguration(); |
| } finally { |
| endRead(); |
| } |
| } |
| |
| /** |
| * Gets 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(final String name) { |
| beginRead(true); |
| try { |
| return namedConfigurations.get(name); |
| } finally { |
| endRead(); |
| } |
| } |
| |
| /** |
| * Gets 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 { |
| return configurations.stream().map(ConfigData::getName).collect(Collectors.toList()); |
| } finally { |
| endRead(); |
| } |
| } |
| |
| /** |
| * Gets 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(); |
| } |
| } |
| |
| /** |
| * Gets 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 { |
| return configurations.stream().map(ConfigData::getConfiguration).collect(Collectors.toList()); |
| } finally { |
| endRead(); |
| } |
| } |
| |
| /** |
| * Gets 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(); |
| } |
| } |
| |
| /** |
| * Gets 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(); |
| } |
| } |
| |
| /** |
| * Gets 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(); |
| } |
| } |
| |
| /** |
| * Gets 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(); |
| } |
| |
| /** |
| * Gets 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(final String key) { |
| if (key == null) { |
| throw new IllegalArgumentException("Key must not be null!"); |
| } |
| |
| final Set<Configuration> sources = getSources(key); |
| if (sources.isEmpty()) { |
| return null; |
| } |
| final Iterator<Configuration> iterator = sources.iterator(); |
| final Configuration source = iterator.next(); |
| if (iterator.hasNext()) { |
| throw new IllegalArgumentException("The key " + key + " is defined by multiple sources!"); |
| } |
| return source; |
| } |
| |
| /** |
| * Gets 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(final String key) { |
| beginRead(false); |
| try { |
| final List<QueryResult<ImmutableNode>> results = fetchNodeList(key); |
| final Set<Configuration> sources = new HashSet<>(); |
| |
| results.forEach(result -> { |
| final 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(); |
| } |
| } |
| |
| /** |
| * Initializes internal data structures for storing information about child configurations. |
| */ |
| private void initChildCollections() { |
| configurations = new ArrayList<>(); |
| namedConfigurations = new HashMap<>(); |
| } |
| |
| /** |
| * 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(); |
| } |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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(final ConfigurationEvent event) { |
| if (event.isBeforeUpdate()) { |
| invalidate(); |
| } |
| } |
| |
| /** |
| * Registers this combined configuration as listener at the given child configuration. |
| * |
| * @param configuration the child configuration |
| */ |
| private void registerListenerAt(final Configuration configuration) { |
| if (configuration instanceof EventSource) { |
| ((EventSource) configuration).addEventListener(ConfigurationEvent.ANY, this); |
| } |
| } |
| |
| /** |
| * 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(final Configuration config) { |
| for (int index = 0; index < getNumberOfConfigurations(); index++) { |
| if (configurations.get(index).getConfiguration() == config) { |
| removeConfigurationAt(index); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * 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(final String name) { |
| final Configuration conf = getConfiguration(name); |
| if (conf != null) { |
| removeConfiguration(conf); |
| } |
| return conf; |
| } |
| |
| /** |
| * Removes the configuration at the specified index. |
| * |
| * @param index the index |
| * @return the removed configuration |
| */ |
| public Configuration removeConfigurationAt(final int index) { |
| final ConfigData cd = configurations.remove(index); |
| if (cd.getName() != null) { |
| namedConfigurations.remove(cd.getName()); |
| } |
| unregisterListenerAt(cd.getConfiguration()); |
| invalidateInternal(); |
| return cd.getConfiguration(); |
| } |
| |
| /** |
| * 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(final ExpressionEngine conversionExpressionEngine) { |
| beginWrite(true); |
| try { |
| this.conversionExpressionEngine = conversionExpressionEngine; |
| } finally { |
| endWrite(); |
| } |
| } |
| |
| /** |
| * 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(final NodeCombiner nodeCombiner) { |
| if (nodeCombiner == null) { |
| throw new IllegalArgumentException("Node combiner must not be null!"); |
| } |
| |
| beginWrite(true); |
| try { |
| this.nodeCombiner = nodeCombiner; |
| invalidateInternal(); |
| } finally { |
| endWrite(); |
| } |
| } |
| |
| /** |
| * Removes this combined configuration as listener from the given child configuration. |
| * |
| * @param configuration the child configuration |
| */ |
| private void unregisterListenerAt(final 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) { |
| configurations.forEach(child -> unregisterListenerAt(child.getConfiguration())); |
| } |
| } |
| } |