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