    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *     https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.configuration2;
    018
    019import java.io.ByteArrayOutputStream;
    020import java.io.PrintStream;
    021import java.util.ArrayList;
    022import java.util.Collection;
    023import java.util.HashMap;
    024import java.util.HashSet;
    025import java.util.Iterator;
    026import java.util.List;
    027import java.util.Map;
    028import java.util.Set;
    029import java.util.stream.Collectors;
    030
    031import org.apache.commons.configuration2.event.ConfigurationEvent;
    032import org.apache.commons.configuration2.event.EventListener;
    033import org.apache.commons.configuration2.event.EventSource;
    034import org.apache.commons.configuration2.event.EventType;
    035import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
    036import org.apache.commons.configuration2.sync.LockMode;
    037import org.apache.commons.configuration2.tree.DefaultConfigurationKey;
    038import org.apache.commons.configuration2.tree.DefaultExpressionEngine;
    039import org.apache.commons.configuration2.tree.ExpressionEngine;
    040import org.apache.commons.configuration2.tree.ImmutableNode;
    041import org.apache.commons.configuration2.tree.NodeCombiner;
    042import org.apache.commons.configuration2.tree.NodeTreeWalker;
    043import org.apache.commons.configuration2.tree.QueryResult;
    044import org.apache.commons.configuration2.tree.TreeUtils;
    045import org.apache.commons.configuration2.tree.UnionCombiner;
    046import org.apache.commons.lang3.StringUtils;
    047
    048/**
    049 * <p>
    050 * A hierarchical composite configuration class.
    051 * </p>
    052 * <p>
    053 * This class maintains a list of configuration objects, which can be added using the diverse {@code addConfiguration()}
    054 * methods. After that the configurations can be accessed either by name (if one was provided when the configuration was
    055 * added) or by index. For the whole set of managed configurations a logical node structure is constructed. For this
    056 * purpose a {@link org.apache.commons.configuration2.tree.NodeCombiner NodeCombiner} object can be set. This makes it
    057 * possible to specify different algorithms for the combination process.
    058 * </p>
    059 * <p>
    060 * The big advantage of this class is that it creates a truly hierarchical structure of all the properties stored in the
    061 * contained configurations - even if some of them are no hierarchical configurations per se. So all enhanced features
    062 * provided by a hierarchical configuration (for example choosing an expression engine) are applicable.
    063 * </p>
    064 * <p>
    065 * The class works by registering itself as an event listener at all added configurations. So it gets notified whenever
    066 * one of these configurations is changed and can invalidate its internal node structure. The next time a property is
    067 * accessed the node structure will be re-constructed using the current state of the managed configurations. Note that,
    068 * depending on the used {@code NodeCombiner}, this may be a complex operation.
    069 * </p>
    070 * <p>
    071 * Because of the way a {@code CombinedConfiguration} is working it has more or less view character: it provides a logic
    072 * view on the configurations it contains. In this constellation not all methods defined for hierarchical configurations
    073 * - especially methods that update the stored properties - can be implemented in a consistent manner. Using such
    074 * methods (like {@code addProperty()}, or {@code clearProperty()} on a {@code CombinedConfiguration} is not strictly
    075 * forbidden, however, depending on the current {@link NodeCombiner} and the involved properties, the results may be
    076 * different than expected. Some examples may illustrate this:
    077 * </p>
    078 * <ul>
    079 * <li>Imagine a {@code CombinedConfiguration} <em>cc</em> containing two child configurations with the following
    080 * content:
    081 * <dl>
    082 * <dt>user.properties</dt>
    083 * <dd>
    084 *
    085 * <pre>
    086 * gui.background = blue
    087 * gui.position = (10, 10, 400, 200)
    088 * </pre>
    089 *
    090 * </dd>
    091 * <dt>default.properties</dt>
    092 * <dd>
    093 *
    094 * <pre>
    095 * gui.background = black
    096 * gui.foreground = white
    097 * home.dir = /data
    098 * </pre>
    099 *
    100 * </dd>
    101 * </dl>
    102 * As a {@code NodeCombiner} a {@link org.apache.commons.configuration2.tree.OverrideCombiner OverrideCombiner} is used.
    103 * This combiner will ensure that defined user settings take precedence over the default values. If the resulting
    104 * {@code CombinedConfiguration} is queried for the background color, {@code blue} will be returned because this value
    105 * is defined in {@code user.properties}. Now consider what happens if the key {@code gui.background} is removed from
    106 * the {@code CombinedConfiguration}:
    107 *
    108 * <pre>
    109 * cc.clearProperty(&quot;gui.background&quot;);
    110 * </pre>
    111 *
    112 * Will a {@code cc.containsKey("gui.background")} now return <strong>false</strong>? No, it won't! The {@code clearProperty()}
    113 * operation is executed on the node set of the combined configuration, which was constructed from the nodes of the two
    114 * child configurations. It causes the value of the <em>background</em> node to be cleared, which is also part of the
    115 * first child configuration. This modification of one of its child configurations causes the
    116 * {@code CombinedConfiguration} to be re-constructed. This time the {@code OverrideCombiner} cannot find a
    117 * {@code gui.background} property in the first child configuration, but it finds one in the second, and adds it to the
    118 * resulting combined configuration. So the property is still present (with a different value now).</li>
    119 * <li>{@code addProperty()} can also be problematic: Most node combiners use special view nodes for linking parts of
    120 * the original configurations' data together. If new properties are added to such a special node, they do not belong to
    121 * any of the managed configurations and thus hang in the air. Using the same configurations as in the last example, the
    122 * statement
    123 *
    124 * <pre>
    125 * addProperty(&quot;database.user&quot;, &quot;scott&quot;);
    126 * </pre>
    127 *
    128 * would cause such a hanging property. If now one of the child configurations is changed and the
    129 * {@code CombinedConfiguration} is re-constructed, this property will disappear! (Add operations are not problematic if
    130 * they result in a child configuration being updated. For instance an {@code addProperty("home.url", "localhost");}
    131 * will alter the second child configuration - because the prefix <em>home</em> is here already present; when the
    132 * {@code CombinedConfiguration} is re-constructed, this change is taken into account.)</li>
    133 * </ul>
    134 * <p>
    135 * Because of such problems it is recommended to perform updates only on the managed child configurations.
    136 * </p>
    137 * <p>
    138 * Whenever the node structure of a {@code CombinedConfiguration} becomes invalid (either because one of the contained
    139 * configurations was modified or because the {@code invalidate()} method was directly called) an event is generated. So
    140 * this can be detected by interested event listeners. This also makes it possible to add a combined configuration into
    141 * another one.
    142 * </p>
    143 * <p>
    144 * Notes about thread-safety: This configuration implementation uses a {@code Synchronizer} object to protect instances
    145 * against concurrent access. The concrete {@code Synchronizer} implementation used determines whether an instance of
    146 * this class is thread-safe or not. In contrast to other implementations derived from
    147 * {@link BaseHierarchicalConfiguration}, thread-safety is an issue here because the nodes structure used by this
    148 * configuration has to be constructed dynamically when a child configuration is changed. Therefore, when multiple
    149 * threads are involved which also manipulate one of the child configurations, a proper {@code Synchronizer} object
    150 * should be set. Note that the {@code Synchronizer} objects used by the child configurations do not really matter.
    151 * Because immutable in-memory nodes structures are used for them there is no danger that updates on child
    152 * configurations could interfere with read operations on the combined configuration.
    153 * </p>
    154 *
    155 * @since 1.3
    156 */
    157public class CombinedConfiguration extends BaseHierarchicalConfiguration implements EventListener<ConfigurationEvent> {
    158
    159    /**
    160     * An internal helper class for storing information about contained configurations.
    161     */
    162    private final class ConfigData {
    163
    164        /** Stores a reference to the configuration. */
    165        private final Configuration configuration;
    166
    167        /** Stores the name under which the configuration is stored. */
    168        private final String name;
    169
    170        /** Stores the at information as path of nodes. */
    171        private final Collection<String> atPath;
    172
    173        /** Stores the at string. */
    174        private final String at;
    175
    176        /** Stores the root node for this child configuration. */
    177        private ImmutableNode rootNode;
    178
    179        /**
    180         * Creates a new instance of {@code ConfigData} and initializes it.
    181         *
    182         * @param config the configuration
    183         * @param n the name
    184         * @param at the at position
    185         */
    186        public ConfigData(final Configuration config, final String n, final String at) {
    187            configuration = config;
    188            name = n;
    189            atPath = parseAt(at);
    190            this.at = at;
    191        }
    192
    193        /**
    194         * Gets the at position of this configuration.
    195         *
    196         * @return the at position
    197         */
    198        public String getAt() {
    199            return at;
    200        }
    201
    202        /**
    203         * Gets the stored configuration.
    204         *
    205         * @return the configuration
    206         */
    207        public Configuration getConfiguration() {
    208            return configuration;
    209        }
    210
    211        /**
    212         * Gets the configuration's name.
    213         *
    214         * @return the name
    215         */
    216        public String getName() {
    217            return name;
    218        }
    219
    220        /**
    221         * Gets the root node for this child configuration.
    222         *
    223         * @return the root node of this child configuration
    224         * @since 1.5
    225         */
    226        public ImmutableNode getRootNode() {
    227            return rootNode;
    228        }
    229
    230        /**
    231         * Obtains the root node of the wrapped configuration. If necessary, a hierarchical representation of the configuration
    232         * has to be created first.
    233         *
    234         * @return the root node of the associated configuration
    235         */
    236        private ImmutableNode getRootNodeOfConfiguration() {
    237            getConfiguration().lock(LockMode.READ);
    238            try {
    239                final ImmutableNode root = ConfigurationUtils.convertToHierarchical(getConfiguration(), conversionExpressionEngine).getNodeModel()
    240                    .getInMemoryRepresentation();
    241                rootNode = root;
    242                return root;
    243            } finally {
    244                getConfiguration().unlock(LockMode.READ);
    245            }
    246        }
    247
    248        /**
    249         * Gets the transformed root node of the stored configuration. The term &quot;transformed&quot; means that an
    250         * eventually defined at path has been applied.
    251         *
    252         * @return the transformed root node
    253         */
    254        public ImmutableNode getTransformedRoot() {
    255            final ImmutableNode configRoot = getRootNodeOfConfiguration();
    256            return atPath == null ? configRoot : prependAtPath(configRoot);
    257        }
    258
    259        /**
    260         * Splits the at path into its components.
    261         *
    262         * @param at the at string
    263         * @return a collection with the names of the single components
    264         */
    265        private Collection<String> parseAt(final String at) {
    266            if (StringUtils.isEmpty(at)) {
    267                return null;
    268            }
    269
    270            final Collection<String> result = new ArrayList<>();
    271            final DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(AT_ENGINE, at).iterator();
    272            while (it.hasNext()) {
    273                result.add(it.nextKey());
    274            }
    275            return result;
    276        }
    277
    278        /**
    279         * Prepends the at path to the given node.
    280         *
    281         * @param node the root node of the represented configuration
    282         * @return the new root node including the at path
    283         */
    284        private ImmutableNode prependAtPath(final ImmutableNode node) {
    285            final ImmutableNode.Builder pathBuilder = new ImmutableNode.Builder();
    286            final Iterator<String> pathIterator = atPath.iterator();
    287            prependAtPathComponent(pathBuilder, pathIterator.next(), pathIterator, node);
    288            return new ImmutableNode.Builder(1).addChild(pathBuilder.create()).create();
    289        }
    290
    291        /**
    292         * Handles a single component of the at path. A corresponding node is created and added to the hierarchical path to the
    293         * original root node of the configuration.
    294         *
    295         * @param builder the current node builder object
    296         * @param currentComponent the name of the current path component
    297         * @param components an iterator with all components of the at path
    298         * @param orgRoot the original root node of the wrapped configuration
    299         */
    300        private void prependAtPathComponent(final ImmutableNode.Builder builder, final String currentComponent, final Iterator<String> components,
    301            final ImmutableNode orgRoot) {
    302            builder.name(currentComponent);
    303            if (components.hasNext()) {
    304                final ImmutableNode.Builder childBuilder = new ImmutableNode.Builder();
    305                prependAtPathComponent(childBuilder, components.next(), components, orgRoot);
    306                builder.addChild(childBuilder.create());
    307            } else {
    308                builder.addChildren(orgRoot.getChildren());
    309                builder.addAttributes(orgRoot.getAttributes());
    310                builder.value(orgRoot.getValue());
    311            }
    312        }
    313    }
    314
    315    /**
    316     * Constant for the event type fired when the internal node structure of a combined configuration becomes invalid.
    317     *
    318     * @since 2.0
    319     */
    320    public static final EventType<ConfigurationEvent> COMBINED_INVALIDATE = new EventType<>(ConfigurationEvent.ANY, "COMBINED_INVALIDATE");
    321
    322    /** Constant for the expression engine for parsing the at path. */
    323    private static final DefaultExpressionEngine AT_ENGINE = DefaultExpressionEngine.INSTANCE;
    324
    325    /** Constant for the default node combiner. */
    326    private static final NodeCombiner DEFAULT_COMBINER = new UnionCombiner();
    327
    328    /** Constant for a root node for an empty configuration. */
    329    private static final ImmutableNode EMPTY_ROOT = new ImmutableNode.Builder().create();
    330
    331    /** Stores the combiner. */
    332    private NodeCombiner nodeCombiner;
    333
    334    /** Stores a list with the contained configurations. */
    335    private List<ConfigData> configurations;
    336
    337    /** Stores a map with the named configurations. */
    338    private Map<String, Configuration> namedConfigurations;
    339
    340    /**
    341     * An expression engine used for converting child configurations to hierarchical ones.
    342     */
    343    private ExpressionEngine conversionExpressionEngine;
    344
    345    /** A flag whether this configuration is up-to-date. */
    346    private boolean upToDate;
    347
    348    /**
    349     * Creates a new instance of {@code CombinedConfiguration} that uses a union combiner.
    350     *
    351     * @see org.apache.commons.configuration2.tree.UnionCombiner
    352     */
    353    public CombinedConfiguration() {
    354        this(null);
    355    }
    356
    357    /**
    358     * Creates a new instance of {@code CombinedConfiguration} and initializes the combiner to be used.
    359     *
    360     * @param comb the node combiner (can be <strong>null</strong>, then a union combiner is used as default)
    361     */
    362    public CombinedConfiguration(final NodeCombiner comb) {
    363        nodeCombiner = comb != null ? comb : DEFAULT_COMBINER;
    364        initChildCollections();
    365    }
    366
    367    /**
    368     * Adds a new configuration to this combined configuration. The new configuration is not given a name. Its properties
    369     * will be added under the root of the combined node structure.
    370     *
    371     * @param config the configuration to add (must not be <strong>null</strong>)
    372     */
    373    public void addConfiguration(final Configuration config) {
    374        addConfiguration(config, null, null);
    375    }
    376
    377    /**
    378     * Adds a new configuration to this combined configuration with an optional name. The new configuration's properties
    379     * will be added under the root of the combined node structure.
    380     *
    381     * @param config the configuration to add (must not be <strong>null</strong>)
    382     * @param name the name of this configuration (can be <strong>null</strong>)
    383     */
    384    public void addConfiguration(final Configuration config, final String name) {
    385        addConfiguration(config, name, null);
    386    }
    387
    388    /**
    389     * Adds a new configuration to this combined configuration. It is possible (but not mandatory) to give the new
    390     * configuration a name. This name must be unique, otherwise a {@code ConfigurationRuntimeException} will be thrown.
    391     * With the optional {@code at} argument you can specify where in the resulting node structure the content of the added
    392     * configuration should appear. This is a string that uses dots as property delimiters (independent on the current
    393     * expression engine). For instance if you pass in the string {@code "database.tables"}, all properties of the added
    394     * configuration will occur in this branch.
    395     *
    396     * @param config the configuration to add (must not be <strong>null</strong>)
    397     * @param name the name of this configuration (can be <strong>null</strong>)
    398     * @param at the position of this configuration in the combined tree (can be <strong>null</strong>)
    399     */
    400    public void addConfiguration(final Configuration config, final String name, final String at) {
    401        if (config == null) {
    402            throw new IllegalArgumentException("Added configuration must not be null.");
    403        }
    404
    405        beginWrite(true);
    406        try {
    407            if (name != null && namedConfigurations.containsKey(name)) {
    408                throw new ConfigurationRuntimeException("A configuration with the name '" + name + "' already exists in this combined configuration.");
    409            }
    410
    411            final ConfigData cd = new ConfigData(config, name, at);
    412            if (getLogger().isDebugEnabled()) {
    413                getLogger().debug("Adding configuration " + config + " with name " + name);
    414            }
    415            configurations.add(cd);
    416            if (name != null) {
    417                namedConfigurations.put(name, config);
    418            }
    419
    420            invalidateInternal();
    421        } finally {
    422            endWrite();
    423        }
    424        registerListenerAt(config);
    425    }
    426
    427    /**
    428     * {@inheritDoc} This implementation checks whether a combined root node is available. If not, it is constructed by
    429     * requesting a write lock.
    430     */
    431    @Override
    432    protected void beginRead(final boolean optimize) {
    433        if (optimize) {
    434            // just need a lock, don't construct configuration
    435            super.beginRead(true);
    436            return;
    437        }
    438
    439        boolean lockObtained = false;
    440        do {
    441            super.beginRead(false);
    442            if (isUpToDate()) {
    443                lockObtained = true;
    444            } else {
    445                // release read lock and try to obtain a write lock
    446                endRead();
    447                beginWrite(false); // this constructs the root node
    448                endWrite();
    449            }
    450        } while (!lockObtained);
    451    }
    452
    453    /**
    454     * {@inheritDoc} This implementation checks whether a combined root node is available. If not, it is constructed now.
    455     */
    456    @Override
    457    protected void beginWrite(final boolean optimize) {
    458        super.beginWrite(true);
    459        if (optimize) {
    460            // just need a lock, don't construct configuration
    461            return;
    462        }
    463
    464        boolean success = false;
    465        try {
    466            if (!isUpToDate()) {
    467                getSubConfigurationParentModel().replaceRoot(constructCombinedNode(), this);
    468                upToDate = true;
    469            }
    470            success = true;
    471        } finally {
    472            if (!success) {
    473                endWrite();
    474            }
    475        }
    476    }
    477
    478    /**
    479     * Clears this configuration. All contained configurations will be removed.
    480     */
    481    @Override
    482    protected void clearInternal() {
    483        unregisterListenerAtChildren();
    484        initChildCollections();
    485        invalidateInternal();
    486    }
    487
    488    /**
    489     * Returns a copy of this object. This implementation performs a deep clone, i.e. all contained configurations will be
    490     * cloned, too. For this to work, all contained configurations must be cloneable. Registered event listeners won't be
    491     * cloned. The clone will use the same node combiner than the original.
    492     *
    493     * @return the copied object
    494     */
    495    @Override
    496    public Object clone() {
    497        beginRead(false);
    498        try {
    499            final CombinedConfiguration copy = (CombinedConfiguration) super.clone();
    500            copy.initChildCollections();
    501            configurations.forEach(cd -> copy.addConfiguration(ConfigurationUtils.cloneConfiguration(cd.getConfiguration()), cd.getName(), cd.getAt()));
    502
    503            return copy;
    504        } finally {
    505            endRead();
    506        }
    507    }
    508
    509    /**
    510     * Creates the root node of this combined configuration.
    511     *
    512     * @return the combined root node
    513     */
    514    private ImmutableNode constructCombinedNode() {
    515        if (getNumberOfConfigurationsInternal() < 1) {
    516            if (getLogger().isDebugEnabled()) {
    517                getLogger().debug("No configurations defined for " + this);
    518            }
    519            return EMPTY_ROOT;
    520        }
    521        final Iterator<ConfigData> it = configurations.iterator();
    522        ImmutableNode node = it.next().getTransformedRoot();
    523        while (it.hasNext()) {
    524            node = nodeCombiner.combine(node, it.next().getTransformedRoot());
    525        }
    526        if (getLogger().isDebugEnabled()) {
    527            final ByteArrayOutputStream os = new ByteArrayOutputStream();
    528            final PrintStream stream = new PrintStream(os);
    529            TreeUtils.printTree(stream, node);
    530            getLogger().debug(os.toString());
    531        }
    532        return node;
    533    }
    534
    535    /**
    536     * Determines the configurations to which the specified node belongs. This is done by inspecting the nodes structures of
    537     * all child configurations.
    538     *
    539     * @param node the node
    540     * @return a set with the owning configurations
    541     */
    542    private Set<Configuration> findSourceConfigurations(final ImmutableNode node) {
    543        final Set<Configuration> result = new HashSet<>();
    544        final FindNodeVisitor<ImmutableNode> visitor = new FindNodeVisitor<>(node);
    545
    546        configurations.forEach(cd -> {
    547            NodeTreeWalker.INSTANCE.walkBFS(cd.getRootNode(), visitor, getModel().getNodeHandler());
    548            if (visitor.isFound()) {
    549                result.add(cd.getConfiguration());
    550                visitor.reset();
    551            }
    552        });
    553
    554        return result;
    555    }
    556
    557    /**
    558     * Gets the configuration at the specified index. The contained configurations are numbered in the order they were
    559     * added to this combined configuration. The index of the first configuration is 0.
    560     *
    561     * @param index the index
    562     * @return the configuration at this index
    563     */
    564    public Configuration getConfiguration(final int index) {
    565        beginRead(true);
    566        try {
    567            final ConfigData cd = configurations.get(index);
    568            return cd.getConfiguration();
    569        } finally {
    570            endRead();
    571        }
    572    }
    573
    574    /**
    575     * Gets the configuration with the given name. This can be <strong>null</strong> if no such configuration exists.
    576     *
    577     * @param name the name of the configuration
    578     * @return the configuration with this name
    579     */
    580    public Configuration getConfiguration(final String name) {
    581        beginRead(true);
    582        try {
    583            return namedConfigurations.get(name);
    584        } finally {
    585            endRead();
    586        }
    587    }
    588
    589    /**
    590     * Gets a List of the names of all the configurations that have been added in the order they were added. A NULL value
    591     * will be present in the list for each configuration that was added without a name.
    592     *
    593     * @return A List of all the configuration names.
    594     * @since 1.7
    595     */
    596    public List<String> getConfigurationNameList() {
    597        beginRead(true);
    598        try {
    599            return configurations.stream().map(ConfigData::getName).collect(Collectors.toList());
    600        } finally {
    601            endRead();
    602        }
    603    }
    604
    605    /**
    606     * Gets a set with the names of all configurations contained in this combined configuration. Of course here are only
    607     * these configurations listed, for which a name was specified when they were added.
    608     *
    609     * @return a set with the names of the contained configurations (never <strong>null</strong>)
    610     */
    611    public Set<String> getConfigurationNames() {
    612        beginRead(true);
    613        try {
    614            return namedConfigurations.keySet();
    615        } finally {
    616            endRead();
    617        }
    618    }
    619
    620    /**
    621     * Gets a List of all the configurations that have been added.
    622     *
    623     * @return A List of all the configurations.
    624     * @since 1.7
    625     */
    626    public List<Configuration> getConfigurations() {
    627        beginRead(true);
    628        try {
    629            return configurations.stream().map(ConfigData::getConfiguration).collect(Collectors.toList());
    630        } finally {
    631            endRead();
    632        }
    633    }
    634
    635    /**
    636     * Gets the {@code ExpressionEngine} for converting flat child configurations to hierarchical ones.
    637     *
    638     * @return the conversion expression engine
    639     * @since 1.6
    640     */
    641    public ExpressionEngine getConversionExpressionEngine() {
    642        beginRead(true);
    643        try {
    644            return conversionExpressionEngine;
    645        } finally {
    646            endRead();
    647        }
    648    }
    649
    650    /**
    651     * Gets the node combiner that is used for creating the combined node structure.
    652     *
    653     * @return the node combiner
    654     */
    655    public NodeCombiner getNodeCombiner() {
    656        beginRead(true);
    657        try {
    658            return nodeCombiner;
    659        } finally {
    660            endRead();
    661        }
    662    }
    663
    664    /**
    665     * Gets the number of configurations that are contained in this combined configuration.
    666     *
    667     * @return the number of contained configurations
    668     */
    669    public int getNumberOfConfigurations() {
    670        beginRead(true);
    671        try {
    672            return getNumberOfConfigurationsInternal();
    673        } finally {
    674            endRead();
    675        }
    676    }
    677
    678    /**
    679     * Gets the number of child configurations in this combined configuration. The internal list of child configurations
    680     * is accessed without synchronization.
    681     *
    682     * @return the number of child configurations
    683     */
    684    private int getNumberOfConfigurationsInternal() {
    685        return configurations.size();
    686    }
    687
    688    /**
    689     * Gets the configuration source, in which the specified key is defined. This method will determine the configuration
    690     * node that is identified by the given key. The following constellations are possible:
    691     * <ul>
    692     * <li>If no node object is found for this key, <strong>null</strong> is returned.</li>
    693     * <li>If the key maps to multiple nodes belonging to different configuration sources, a
    694     * {@code IllegalArgumentException} is thrown (in this case no unique source can be determined).</li>
    695     * <li>If exactly one node is found for the key, the (child) configuration object, to which the node belongs is
    696     * determined and returned.</li>
    697     * <li>For keys that have been added directly to this combined configuration and that do not belong to the namespaces
    698     * defined by existing child configurations this configuration will be returned.</li>
    699     * </ul>
    700     *
    701     * @param key the key of a configuration property
    702     * @return the configuration, to which this property belongs or <strong>null</strong> if the key cannot be resolved
    703     * @throws IllegalArgumentException if the key maps to multiple properties and the source cannot be determined, or if
    704     *         the key is <strong>null</strong>
    705     * @since 1.5
    706     */
    707    public Configuration getSource(final String key) {
    708        if (key == null) {
    709            throw new IllegalArgumentException("Key must not be null.");
    710        }
    711
    712        final Set<Configuration> sources = getSources(key);
    713        if (sources.isEmpty()) {
    714            return null;
    715        }
    716        final Iterator<Configuration> iterator = sources.iterator();
    717        final Configuration source = iterator.next();
    718        if (iterator.hasNext()) {
    719            throw new IllegalArgumentException("The key " + key + " is defined by multiple sources.");
    720        }
    721        return source;
    722    }
    723
    724    /**
    725     * Gets a set with the configuration sources, in which the specified key is defined. This method determines the
    726     * configuration nodes that are identified by the given key. It then determines the configuration sources to which these
    727     * nodes belong and adds them to the result set. Note the following points:
    728     * <ul>
    729     * <li>If no node object is found for this key, an empty set is returned.</li>
    730     * <li>For keys that have been added directly to this combined configuration and that do not belong to the namespaces
    731     * defined by existing child configurations this combined configuration is contained in the result set.</li>
    732     * </ul>
    733     *
    734     * @param key the key of a configuration property
    735     * @return a set with the configuration sources, which contain this property
    736     * @since 2.0
    737     */
    738    public Set<Configuration> getSources(final String key) {
    739        beginRead(false);
    740        try {
    741            final List<QueryResult<ImmutableNode>> results = fetchNodeList(key);
    742            final Set<Configuration> sources = new HashSet<>();
    743
    744            results.forEach(result -> {
    745                final Set<Configuration> resultSources = findSourceConfigurations(result.getNode());
    746                if (resultSources.isEmpty()) {
    747                    // key must be defined in combined configuration
    748                    sources.add(this);
    749                } else {
    750                    sources.addAll(resultSources);
    751                }
    752            });
    753
    754            return sources;
    755        } finally {
    756            endRead();
    757        }
    758    }
    759
    760    /**
    761     * Initializes internal data structures for storing information about child configurations.
    762     */
    763    private void initChildCollections() {
    764        configurations = new ArrayList<>();
    765        namedConfigurations = new HashMap<>();
    766    }
    767
    768    /**
    769     * Invalidates this combined configuration. This means that the next time a property is accessed the combined node
    770     * structure must be re-constructed. Invalidation of a combined configuration also means that an event of type
    771     * {@code EVENT_COMBINED_INVALIDATE} is fired. Note that while other events most times appear twice (once before and
    772     * once after an update), this event is only fired once (after update).
    773     */
    774    public void invalidate() {
    775        beginWrite(true);
    776        try {
    777            invalidateInternal();
    778        } finally {
    779            endWrite();
    780        }
    781    }
    782
    783    /**
    784     * Marks this configuration as invalid. This means that the next access re-creates the root node. An invalidate event is
    785     * also fired. Note: This implementation expects that an exclusive (write) lock is held on this instance.
    786     */
    787    private void invalidateInternal() {
    788        upToDate = false;
    789        fireEvent(COMBINED_INVALIDATE, null, null, false);
    790    }
    791
    792    /**
    793     * Returns a flag whether this configuration has been invalidated. This means that the combined nodes structure has to
    794     * be rebuilt before the configuration can be accessed.
    795     *
    796     * @return a flag whether this configuration is invalid
    797     */
    798    private boolean isUpToDate() {
    799        return upToDate;
    800    }
    801
    802    /**
    803     * Event listener call back for configuration update events. This method is called whenever one of the contained
    804     * configurations was modified. It invalidates this combined configuration.
    805     *
    806     * @param event the update event
    807     */
    808    @Override
    809    public void onEvent(final ConfigurationEvent event) {
    810        if (event.isBeforeUpdate()) {
    811            invalidate();
    812        }
    813    }
    814
    815    /**
    816     * Registers this combined configuration as listener at the given child configuration.
    817     *
    818     * @param configuration the child configuration
    819     */
    820    private void registerListenerAt(final Configuration configuration) {
    821        if (configuration instanceof EventSource) {
    822            ((EventSource) configuration).addEventListener(ConfigurationEvent.ANY, this);
    823        }
    824    }
    825
    826    /**
    827     * Removes the specified configuration from this combined configuration.
    828     *
    829     * @param config the configuration to be removed
    830     * @return a flag whether this configuration was found and could be removed
    831     */
    832    public boolean removeConfiguration(final Configuration config) {
    833        for (int index = 0; index < getNumberOfConfigurations(); index++) {
    834            if (configurations.get(index).getConfiguration() == config) {
    835                removeConfigurationAt(index);
    836                return true;
    837            }
    838        }
    839
    840        return false;
    841    }
    842
    843    /**
    844     * Removes the configuration with the specified name.
    845     *
    846     * @param name the name of the configuration to be removed
    847     * @return the removed configuration (<strong>null</strong> if this configuration was not found)
    848     */
    849    public Configuration removeConfiguration(final String name) {
    850        final Configuration conf = getConfiguration(name);
    851        if (conf != null) {
    852            removeConfiguration(conf);
    853        }
    854        return conf;
    855    }
    856
    857    /**
    858     * Removes the configuration at the specified index.
    859     *
    860     * @param index the index
    861     * @return the removed configuration
    862     */
    863    public Configuration removeConfigurationAt(final int index) {
    864        final ConfigData cd = configurations.remove(index);
    865        if (cd.getName() != null) {
    866            namedConfigurations.remove(cd.getName());
    867        }
    868        unregisterListenerAt(cd.getConfiguration());
    869        invalidateInternal();
    870        return cd.getConfiguration();
    871    }
    872
    873    /**
    874     * Sets the {@code ExpressionEngine} for converting flat child configurations to hierarchical ones. When constructing
    875     * the root node for this combined configuration the properties of all child configurations must be combined to a single
    876     * hierarchical node structure. In this process, non hierarchical configurations are converted to hierarchical ones
    877     * first. This can be problematic if a child configuration contains keys that are no compatible with the default
    878     * expression engine used by hierarchical configurations. Therefore it is possible to specify a specific expression
    879     * engine to be used for this purpose.
    880     *
    881     * @param conversionExpressionEngine the conversion expression engine
    882     * @see ConfigurationUtils#convertToHierarchical(Configuration, ExpressionEngine)
    883     * @since 1.6
    884     */
    885    public void setConversionExpressionEngine(final ExpressionEngine conversionExpressionEngine) {
    886        beginWrite(true);
    887        try {
    888            this.conversionExpressionEngine = conversionExpressionEngine;
    889        } finally {
    890            endWrite();
    891        }
    892    }
    893
    894    /**
    895     * Sets the node combiner. This object will be used when the combined node structure is to be constructed. It must not
    896     * be <strong>null</strong>, otherwise an {@code IllegalArgumentException} exception is thrown. Changing the node combiner causes
    897     * an invalidation of this combined configuration, so that the new combiner immediately takes effect.
    898     *
    899     * @param nodeCombiner the node combiner
    900     */
    901    public void setNodeCombiner(final NodeCombiner nodeCombiner) {
    902        if (nodeCombiner == null) {
    903            throw new IllegalArgumentException("Node combiner must not be null.");
    904        }
    905
    906        beginWrite(true);
    907        try {
    908            this.nodeCombiner = nodeCombiner;
    909            invalidateInternal();
    910        } finally {
    911            endWrite();
    912        }
    913    }
    914
    915    /**
    916     * Removes this combined configuration as listener from the given child configuration.
    917     *
    918     * @param configuration the child configuration
    919     */
    920    private void unregisterListenerAt(final Configuration configuration) {
    921        if (configuration instanceof EventSource) {
    922            ((EventSource) configuration).removeEventListener(ConfigurationEvent.ANY, this);
    923        }
    924    }
    925
    926    /**
    927     * Removes this combined configuration as listener from all child configurations. This method is called on a clear()
    928     * operation.
    929     */
    930    private void unregisterListenerAtChildren() {
    931        if (configurations != null) {
    932            configurations.forEach(child -> unregisterListenerAt(child.getConfiguration()));
    933        }
    934    }
    935}