001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.configuration2; 018 019import java.io.ByteArrayOutputStream; 020import java.io.PrintStream; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.Iterator; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.stream.Collectors; 030 031import org.apache.commons.configuration2.event.ConfigurationEvent; 032import org.apache.commons.configuration2.event.EventListener; 033import org.apache.commons.configuration2.event.EventSource; 034import org.apache.commons.configuration2.event.EventType; 035import org.apache.commons.configuration2.ex.ConfigurationRuntimeException; 036import org.apache.commons.configuration2.sync.LockMode; 037import org.apache.commons.configuration2.tree.DefaultConfigurationKey; 038import org.apache.commons.configuration2.tree.DefaultExpressionEngine; 039import org.apache.commons.configuration2.tree.ExpressionEngine; 040import org.apache.commons.configuration2.tree.ImmutableNode; 041import org.apache.commons.configuration2.tree.NodeCombiner; 042import org.apache.commons.configuration2.tree.NodeTreeWalker; 043import org.apache.commons.configuration2.tree.QueryResult; 044import org.apache.commons.configuration2.tree.TreeUtils; 045import org.apache.commons.configuration2.tree.UnionCombiner; 046import org.apache.commons.lang3.StringUtils; 047 048/** 049 * <p> 050 * A hierarchical composite configuration class. 051 * </p> 052 * <p> 053 * This class maintains a list of configuration objects, which can be added using the diverse {@code addConfiguration()} 054 * methods. After that the configurations can be accessed either by name (if one was provided when the configuration was 055 * added) or by index. For the whole set of managed configurations a logical node structure is constructed. For this 056 * purpose a {@link org.apache.commons.configuration2.tree.NodeCombiner NodeCombiner} object can be set. This makes it 057 * possible to specify different algorithms for the combination process. 058 * </p> 059 * <p> 060 * The big advantage of this class is that it creates a truly hierarchical structure of all the properties stored in the 061 * contained configurations - even if some of them are no hierarchical configurations per se. So all enhanced features 062 * provided by a hierarchical configuration (for example choosing an expression engine) are applicable. 063 * </p> 064 * <p> 065 * The class works by registering itself as an event listener at all added configurations. So it gets notified whenever 066 * one of these configurations is changed and can invalidate its internal node structure. The next time a property is 067 * accessed the node structure will be re-constructed using the current state of the managed configurations. Note that, 068 * depending on the used {@code NodeCombiner}, this may be a complex operation. 069 * </p> 070 * <p> 071 * Because of the way a {@code CombinedConfiguration} is working it has more or less view character: it provides a logic 072 * view on the configurations it contains. In this constellation not all methods defined for hierarchical configurations 073 * - especially methods that update the stored properties - can be implemented in a consistent manner. Using such 074 * methods (like {@code addProperty()}, or {@code clearProperty()} on a {@code CombinedConfiguration} is not strictly 075 * forbidden, however, depending on the current {@link NodeCombiner} and the involved properties, the results may be 076 * different than expected. Some examples may illustrate this: 077 * </p> 078 * <ul> 079 * <li>Imagine a {@code CombinedConfiguration} <em>cc</em> containing two child configurations with the following 080 * content: 081 * <dl> 082 * <dt>user.properties</dt> 083 * <dd> 084 * 085 * <pre> 086 * gui.background = blue 087 * gui.position = (10, 10, 400, 200) 088 * </pre> 089 * 090 * </dd> 091 * <dt>default.properties</dt> 092 * <dd> 093 * 094 * <pre> 095 * gui.background = black 096 * gui.foreground = white 097 * home.dir = /data 098 * </pre> 099 * 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 */ 157public 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}