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