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