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 * http://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 (e.g. 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 <b>false</b>? No, it won't! The {@code clearProperty()} 113 * operation is executed on the node set of the combined configuration, which was constructed from the nodes of the two 114 * child configurations. It causes the value of the <em>background</em> node to be cleared, which is also part of the 115 * first child configuration. This modification of one of its child configurations causes the 116 * {@code CombinedConfiguration} to be re-constructed. This time the {@code OverrideCombiner} cannot find a 117 * {@code gui.background} property in the first child configuration, but it finds one in the second, and adds it to the 118 * resulting combined configuration. So the property is still present (with a different value now).</li> 119 * <li>{@code addProperty()} can also be problematic: Most node combiners use special view nodes for linking parts of 120 * the original configurations' data together. If new properties are added to such a special node, they do not belong to 121 * any of the managed configurations and thus hang in the air. Using the same configurations as in the last example, the 122 * statement 123 * 124 * <pre> 125 * addProperty("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 * 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 <b>null</b>, 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 <b>null</b>) 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 <b>null</b>) 380 * @param name the name of this configuration (can be <b>null</b>) 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 <b>null</b>) 395 * @param name the name of this configuration (can be <b>null</b>) 396 * @param at the position of this configuration in the combined tree (can be <b>null</b>) 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 <b>null</b> 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 <b>null</b>) 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, <b>null</b> 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 <b>null</b> 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 <b>null</b> 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 (<b>null</b> 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 <b>null</b>, 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}