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