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    *     https://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     /**
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 }