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("gui.background");
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("database.user", "scott");
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 "transformed" 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 }