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}