1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.configuration;
18
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26
27 import org.apache.commons.configuration.event.ConfigurationEvent;
28 import org.apache.commons.configuration.event.ConfigurationListener;
29 import org.apache.commons.configuration.tree.ConfigurationNode;
30 import org.apache.commons.configuration.tree.DefaultConfigurationKey;
31 import org.apache.commons.configuration.tree.DefaultConfigurationNode;
32 import org.apache.commons.configuration.tree.DefaultExpressionEngine;
33 import org.apache.commons.configuration.tree.NodeCombiner;
34 import org.apache.commons.configuration.tree.UnionCombiner;
35 import org.apache.commons.configuration.tree.ViewNode;
36
37 /***
38 * <p>
39 * A hierarchical composite configuration class.
40 * </p>
41 * <p>
42 * This class maintains a list of configuration objects, which can be added
43 * using the divers <code>addConfiguration()</code> methods. After that the
44 * configurations can be accessed either by name (if one was provided when the
45 * configuration was added) or by index. For the whole set of managed
46 * configurations a logical node structure is constructed. For this purpose a
47 * <code>{@link org.apache.commons.configuration.tree.NodeCombiner NodeCombiner}</code>
48 * object can be set. This makes it possible to specify different algorithms for
49 * the combination process.
50 * </p>
51 * <p>
52 * The big advantage of this class is that it creates a truly hierarchical
53 * structure of all the properties stored in the contained configurations - even
54 * if some of them are no hierarchical configurations per se. So all enhanced
55 * features provided by a hierarchical configuration (e.g. choosing an
56 * expression engine) are applicable.
57 * </p>
58 * <p>
59 * The class works by registering itself as an event listener at all added
60 * configurations. So it gets notified whenever one of these configurations is
61 * changed and can invalidate its internal node structure. The next time a
62 * property is accessed the node structure will be re-constructed using the
63 * current state of the managed configurations. Note that, depending on the used
64 * <code>NodeCombiner</code>, this may be a complex operation.
65 * </p>
66 * <p>
67 * Because of the way a <code>CombinedConfiguration</code> is working it has
68 * more or less view character: it provides a logic view on the configurations
69 * it contains. In this constellation not all methods defined for hierarchical
70 * configurations - especially methods that update the stored properties - can
71 * be implemented in a consistent manner. Using such methods (like
72 * <code>addProperty()</code>, or <code>clearProperty()</code> on a
73 * <code>CombinedConfiguration</code> is not strictly forbidden, however,
74 * depending on the current <code>{@link NodeCombiner}</code> and the involved
75 * properties, the results may be different than expected. Some examples may
76 * illustrate this:
77 * </p>
78 * <p>
79 * <ul>
80 * <li>Imagine a <code>CombinedConfiguration</code> <em>cc</em> containing
81 * two child configurations with the following content:
82 * <dl>
83 * <dt>user.properties</dt>
84 * <dd>
85 *
86 * <pre>
87 * gui.background = blue
88 * gui.position = (10, 10, 400, 200)
89 * </pre>
90 *
91 * </dd>
92 * <dt>default.properties</dt>
93 * <dd>
94 *
95 * <pre>
96 * gui.background = black
97 * gui.foreground = white
98 * home.dir = /data
99 * </pre>
100 *
101 * </dd>
102 * </dl>
103 * As a <code>NodeCombiner</code> a
104 * <code>{@link org.apache.commons.configuration.tree.OverrideCombiner OverrideCombiner}</code>
105 * is used. This combiner will ensure that defined user settings take precedence
106 * over the default values. If the resulting <code>CombinedConfiguration</code>
107 * is queried for the background color, <code>blue</code> will be returned
108 * because this value is defined in <code>user.properties</code>. Now
109 * consider what happens if the key <code>gui.background</code> is removed
110 * from the <code>CombinedConfiguration</code>:
111 *
112 * <pre>cc.clearProperty("gui.background");</pre>
113 *
114 * Will a <code>cc.containsKey("gui.background")</code> now return <b>false</b>?
115 * No, it won't! The <code>clearProperty()</code> operation is executed on the
116 * node set of the combined configuration, which was constructed from the nodes
117 * of the two child configurations. It causes the value of the
118 * <em>background</em> node to be cleared, which is also part of the first
119 * child configuration. This modification of one of its child configurations
120 * causes the <code>CombinedConfiguration</code> to be re-constructed. This
121 * time the <code>OverrideCombiner</code> cannot find a
122 * <code>gui.background</code> property in the first child configuration, but
123 * it finds one in the second, and adds it to the resulting combined
124 * configuration. So the property is still present (with a different value now).</li>
125 * <li><code>addProperty()</code> can also be problematic: Most node
126 * combiners use special view nodes for linking parts of the original
127 * configurations' data together. If new properties are added to such a special
128 * node, they do not belong to any of the managed configurations and thus hang
129 * in the air. Using the same configurations as in the last example, the
130 * statement
131 *
132 * <pre>
133 * addProperty("database.user", "scott");
134 * </pre>
135 *
136 * would cause such a hanging property. If now one of the child configurations
137 * is changed and the <code>CombinedConfiguration</code> is re-constructed,
138 * this property will disappear! (Add operations are not problematic if they
139 * result in a child configuration being updated. For instance an
140 * <code>addProperty("home.url", "localhost");</code> will alter the second
141 * child configuration - because the prefix <em>home</em> is here already
142 * present; when the <code>CombinedConfiguration</code> is re-constructed,
143 * this change is taken into account.)</li>
144 * </ul>
145 * Because of such problems it is recommended to perform updates only on the
146 * managed child configurations.
147 * </p>
148 * <p>
149 * Whenever the node structure of a <code>CombinedConfiguration</code> becomes
150 * invalid (either because one of the contained configurations was modified or
151 * because the <code>invalidate()</code> method was directly called) an event
152 * is generated. So this can be detected by interested event listeners. This
153 * also makes it possible to add a combined configuration into another one.
154 * </p>
155 * <p>
156 * Implementation note: Adding and removing configurations to and from a
157 * combined configuration is not thread-safe. If a combined configuration is
158 * manipulated by multiple threads, the developer has to take care about
159 * properly synchronization.
160 * </p>
161 *
162 * @author <a
163 * href="http://commons.apache.org/configuration/team-list.html">Commons
164 * Configuration team</a>
165 * @since 1.3
166 * @version $Id: CombinedConfiguration.java 569412 2007-08-24 15:07:53Z oheger $
167 */
168 public class CombinedConfiguration extends HierarchicalConfiguration implements
169 ConfigurationListener, Cloneable
170 {
171 /***
172 * Constant for the invalidate event that is fired when the internal node
173 * structure becomes invalid.
174 */
175 public static final int EVENT_COMBINED_INVALIDATE = 40;
176
177 /***
178 * The serial version ID.
179 */
180 private static final long serialVersionUID = 8338574525528692307L;
181
182 /*** Constant for the expression engine for parsing the at path. */
183 private static final DefaultExpressionEngine AT_ENGINE = new DefaultExpressionEngine();
184
185 /*** Constant for the default node combiner. */
186 private static final NodeCombiner DEFAULT_COMBINER = new UnionCombiner();
187
188 /*** Constant for the name of the property used for the reload check.*/
189 private static final String PROP_RELOAD_CHECK = "CombinedConfigurationReloadCheck";
190
191 /*** Stores the combiner. */
192 private NodeCombiner nodeCombiner;
193
194 /*** Stores the combined root node. */
195 private ConfigurationNode combinedRoot;
196
197 /*** Stores a list with the contained configurations. */
198 private List configurations;
199
200 /*** Stores a map with the named configurations. */
201 private Map namedConfigurations;
202
203 /*** A flag whether an enhanced reload check is to be performed.*/
204 private boolean forceReloadCheck;
205
206 /***
207 * Creates a new instance of <code>CombinedConfiguration</code> and
208 * initializes the combiner to be used.
209 *
210 * @param comb the node combiner (can be <b>null</b>, then a union combiner
211 * is used as default)
212 */
213 public CombinedConfiguration(NodeCombiner comb)
214 {
215 setNodeCombiner((comb != null) ? comb : DEFAULT_COMBINER);
216 clear();
217 }
218
219 /***
220 * Creates a new instance of <code>CombinedConfiguration</code> that uses
221 * a union combiner.
222 *
223 * @see org.apache.commons.configuration.tree.UnionCombiner
224 */
225 public CombinedConfiguration()
226 {
227 this(null);
228 }
229
230 /***
231 * Returns the node combiner that is used for creating the combined node
232 * structure.
233 *
234 * @return the node combiner
235 */
236 public NodeCombiner getNodeCombiner()
237 {
238 return nodeCombiner;
239 }
240
241 /***
242 * Sets the node combiner. This object will be used when the combined node
243 * structure is to be constructed. It must not be <b>null</b>, otherwise an
244 * <code>IllegalArgumentException</code> exception is thrown. Changing the
245 * node combiner causes an invalidation of this combined configuration, so
246 * that the new combiner immediately takes effect.
247 *
248 * @param nodeCombiner the node combiner
249 */
250 public void setNodeCombiner(NodeCombiner nodeCombiner)
251 {
252 if (nodeCombiner == null)
253 {
254 throw new IllegalArgumentException(
255 "Node combiner must not be null!");
256 }
257 this.nodeCombiner = nodeCombiner;
258 invalidate();
259 }
260
261 /***
262 * Returns a flag whether an enhanced reload check must be performed.
263 *
264 * @return the force reload check flag
265 * @since 1.4
266 */
267 public boolean isForceReloadCheck()
268 {
269 return forceReloadCheck;
270 }
271
272 /***
273 * Sets the force reload check flag. If this flag is set, each property
274 * access on this configuration will cause a reload check on the contained
275 * configurations. This is a workaround for a problem with some reload
276 * implementations that only check if a reload is required when they are
277 * triggered. Per default this mode is disabled. If the force reload check
278 * flag is set to <b>true</b>, accessing properties will be less
279 * performant, but reloads on contained configurations will be detected.
280 *
281 * @param forceReloadCheck the value of the flag
282 * @since 1.4
283 */
284 public void setForceReloadCheck(boolean forceReloadCheck)
285 {
286 this.forceReloadCheck = forceReloadCheck;
287 }
288
289 /***
290 * Adds a new configuration to this combined configuration. It is possible
291 * (but not mandatory) to give the new configuration a name. This name must
292 * be unique, otherwise a <code>ConfigurationRuntimeException</code> will
293 * be thrown. With the optional <code>at</code> argument you can specify
294 * where in the resulting node structure the content of the added
295 * configuration should appear. This is a string that uses dots as property
296 * delimiters (independent on the current expression engine). For instance
297 * if you pass in the string <code>"database.tables"</code>,
298 * all properties of the added configuration will occur in this branch.
299 *
300 * @param config the configuration to add (must not be <b>null</b>)
301 * @param name the name of this configuration (can be <b>null</b>)
302 * @param at the position of this configuration in the combined tree (can be
303 * <b>null</b>)
304 */
305 public void addConfiguration(AbstractConfiguration config, String name,
306 String at)
307 {
308 if (config == null)
309 {
310 throw new IllegalArgumentException(
311 "Added configuration must not be null!");
312 }
313 if (name != null && namedConfigurations.containsKey(name))
314 {
315 throw new ConfigurationRuntimeException(
316 "A configuration with the name '"
317 + name
318 + "' already exists in this combined configuration!");
319 }
320
321 ConfigData cd = new ConfigData(config, name, at);
322 configurations.add(cd);
323 if (name != null)
324 {
325 namedConfigurations.put(name, config);
326 }
327
328 config.addConfigurationListener(this);
329 invalidate();
330 }
331
332 /***
333 * Adds a new configuration to this combined configuration with an optional
334 * name. The new configuration's properties will be added under the root of
335 * the combined node structure.
336 *
337 * @param config the configuration to add (must not be <b>null</b>)
338 * @param name the name of this configuration (can be <b>null</b>)
339 */
340 public void addConfiguration(AbstractConfiguration config, String name)
341 {
342 addConfiguration(config, name, null);
343 }
344
345 /***
346 * Adds a new configuration to this combined configuration. The new
347 * configuration is not given a name. Its properties will be added under the
348 * root of the combined node structure.
349 *
350 * @param config the configuration to add (must not be <b>null</b>)
351 */
352 public void addConfiguration(AbstractConfiguration config)
353 {
354 addConfiguration(config, null, null);
355 }
356
357 /***
358 * Returns the number of configurations that are contained in this combined
359 * configuration.
360 *
361 * @return the number of contained configurations
362 */
363 public int getNumberOfConfigurations()
364 {
365 return configurations.size();
366 }
367
368 /***
369 * Returns the configuration at the specified index. The contained
370 * configurations are numbered in the order they were added to this combined
371 * configuration. The index of the first configuration is 0.
372 *
373 * @param index the index
374 * @return the configuration at this index
375 */
376 public Configuration getConfiguration(int index)
377 {
378 ConfigData cd = (ConfigData) configurations.get(index);
379 return cd.getConfiguration();
380 }
381
382 /***
383 * Returns the configuration with the given name. This can be <b>null</b>
384 * if no such configuration exists.
385 *
386 * @param name the name of the configuration
387 * @return the configuration with this name
388 */
389 public Configuration getConfiguration(String name)
390 {
391 return (Configuration) namedConfigurations.get(name);
392 }
393
394 /***
395 * Removes the specified configuration from this combined configuration.
396 *
397 * @param config the configuration to be removed
398 * @return a flag whether this configuration was found and could be removed
399 */
400 public boolean removeConfiguration(Configuration config)
401 {
402 for (int index = 0; index < getNumberOfConfigurations(); index++)
403 {
404 if (((ConfigData) configurations.get(index)).getConfiguration() == config)
405 {
406 removeConfigurationAt(index);
407 return true;
408 }
409 }
410
411 return false;
412 }
413
414 /***
415 * Removes the configuration at the specified index.
416 *
417 * @param index the index
418 * @return the removed configuration
419 */
420 public Configuration removeConfigurationAt(int index)
421 {
422 ConfigData cd = (ConfigData) configurations.remove(index);
423 if (cd.getName() != null)
424 {
425 namedConfigurations.remove(cd.getName());
426 }
427 cd.getConfiguration().removeConfigurationListener(this);
428 invalidate();
429 return cd.getConfiguration();
430 }
431
432 /***
433 * Removes the configuration with the specified name.
434 *
435 * @param name the name of the configuration to be removed
436 * @return the removed configuration (<b>null</b> if this configuration
437 * was not found)
438 */
439 public Configuration removeConfiguration(String name)
440 {
441 Configuration conf = getConfiguration(name);
442 if (conf != null)
443 {
444 removeConfiguration(conf);
445 }
446 return conf;
447 }
448
449 /***
450 * Returns a set with the names of all configurations contained in this
451 * combined configuration. Of course here are only these configurations
452 * listed, for which a name was specified when they were added.
453 *
454 * @return a set with the names of the contained configurations (never
455 * <b>null</b>)
456 */
457 public Set getConfigurationNames()
458 {
459 return namedConfigurations.keySet();
460 }
461
462 /***
463 * Invalidates this combined configuration. This means that the next time a
464 * property is accessed the combined node structure must be re-constructed.
465 * Invalidation of a combined configuration also means that an event of type
466 * <code>EVENT_COMBINED_INVALIDATE</code> is fired. Note that while other
467 * events most times appear twice (once before and once after an update),
468 * this event is only fired once (after update).
469 */
470 public void invalidate()
471 {
472 synchronized (getNodeCombiner())
473 {
474 combinedRoot = null;
475 }
476 fireEvent(EVENT_COMBINED_INVALIDATE, null, null, false);
477 }
478
479 /***
480 * Event listener call back for configuration update events. This method is
481 * called whenever one of the contained configurations was modified. It
482 * invalidates this combined configuration.
483 *
484 * @param event the update event
485 */
486 public void configurationChanged(ConfigurationEvent event)
487 {
488 invalidate();
489 }
490
491 /***
492 * Returns the configuration root node of this combined configuration. This
493 * method will construct a combined node structure using the current node
494 * combiner if necessary.
495 *
496 * @return the combined root node
497 */
498 public ConfigurationNode getRootNode()
499 {
500 synchronized (getNodeCombiner())
501 {
502 if (combinedRoot == null)
503 {
504 combinedRoot = constructCombinedNode();
505 }
506 return combinedRoot;
507 }
508 }
509
510 /***
511 * Clears this configuration. All contained configurations will be removed.
512 */
513 public void clear()
514 {
515 fireEvent(EVENT_CLEAR, null, null, true);
516 configurations = new ArrayList();
517 namedConfigurations = new HashMap();
518 fireEvent(EVENT_CLEAR, null, null, false);
519 invalidate();
520 }
521
522 /***
523 * Returns a copy of this object. This implementation performs a deep clone,
524 * i.e. all contained configurations will be cloned, too. For this to work,
525 * all contained configurations must be cloneable. Registered event
526 * listeners won't be cloned. The clone will use the same node combiner than
527 * the original.
528 *
529 * @return the copied object
530 */
531 public Object clone()
532 {
533 CombinedConfiguration copy = (CombinedConfiguration) super.clone();
534 copy.clear();
535 for (Iterator it = configurations.iterator(); it.hasNext();)
536 {
537 ConfigData cd = (ConfigData) it.next();
538 copy.addConfiguration((AbstractConfiguration) ConfigurationUtils
539 .cloneConfiguration(cd.getConfiguration()), cd.getName(),
540 cd.getAt());
541 }
542
543 copy.setRootNode(new DefaultConfigurationNode());
544 return copy;
545 }
546
547 /***
548 * Returns the value of the specified property. This implementation
549 * evaluates the <em>force reload check</em> flag. If it is set, all
550 * contained configurations will be triggered before the value of the
551 * requested property is retrieved.
552 *
553 * @param key the key of the desired property
554 * @return the value of this property
555 * @since 1.4
556 */
557 public Object getProperty(String key)
558 {
559 if (isForceReloadCheck())
560 {
561 for (Iterator it = configurations.iterator(); it.hasNext();)
562 {
563 try
564 {
565
566
567 ((ConfigData) it.next()).getConfiguration().getProperty(
568 PROP_RELOAD_CHECK);
569 }
570 catch (Exception ex)
571 {
572
573 ;
574 }
575 }
576 }
577
578 return super.getProperty(key);
579 }
580
581 /***
582 * Returns the configuration source, in which the specified key is defined.
583 * This method will determine the configuration node that is identified by
584 * the given key. The following constellations are possible:
585 * <ul>
586 * <li>If no node object is found for this key, <b>null</b> is returned.</li>
587 * <li>If the key maps to multiple nodes belonging to different
588 * configuration sources, a <code>IllegalArgumentException</code> is
589 * thrown (in this case no unique source can be determined).</li>
590 * <li>If exactly one node is found for the key, the (child) configuration
591 * object, to which the node belongs is determined and returned.</li>
592 * <li>For keys that have been added directly to this combined
593 * configuration and that do not belong to the namespaces defined by
594 * existing child configurations this configuration will be returned.</li>
595 * </ul>
596 *
597 * @param key the key of a configuration property
598 * @return the configuration, to which this property belongs or <b>null</b>
599 * if the key cannot be resolved
600 * @throws IllegalArgumentException if the key maps to multiple properties
601 * and the source cannot be determined, or if the key is <b>null</b>
602 * @since 1.5
603 */
604 public Configuration getSource(String key)
605 {
606 if (key == null)
607 {
608 throw new IllegalArgumentException("Key must not be null!");
609 }
610
611 List nodes = fetchNodeList(key);
612 if (nodes.isEmpty())
613 {
614 return null;
615 }
616
617 Iterator it = nodes.iterator();
618 Configuration source = findSourceConfiguration((ConfigurationNode) it
619 .next());
620 while (it.hasNext())
621 {
622 Configuration src = findSourceConfiguration((ConfigurationNode) it
623 .next());
624 if (src != source)
625 {
626 throw new IllegalArgumentException("The key " + key
627 + " is defined by multiple sources!");
628 }
629 }
630
631 return source;
632 }
633
634 /***
635 * Creates the root node of this combined configuration.
636 *
637 * @return the combined root node
638 */
639 private ConfigurationNode constructCombinedNode()
640 {
641 if (getNumberOfConfigurations() < 1)
642 {
643 return new ViewNode();
644 }
645
646 else
647 {
648 Iterator it = configurations.iterator();
649 ConfigurationNode node = ((ConfigData) it.next())
650 .getTransformedRoot();
651 while (it.hasNext())
652 {
653 node = getNodeCombiner().combine(node,
654 ((ConfigData) it.next()).getTransformedRoot());
655 }
656 return node;
657 }
658 }
659
660 /***
661 * Determines the configuration that owns the specified node.
662 *
663 * @param node the node
664 * @return the owning configuration
665 */
666 private Configuration findSourceConfiguration(ConfigurationNode node)
667 {
668 ConfigurationNode root = null;
669 ConfigurationNode current = node;
670
671
672 while (current != null)
673 {
674 root = current;
675 current = current.getParentNode();
676 }
677
678
679 for (Iterator it = configurations.iterator(); it.hasNext();)
680 {
681 ConfigData cd = (ConfigData) it.next();
682 if (root == cd.getRootNode())
683 {
684 return cd.getConfiguration();
685 }
686 }
687
688 return this;
689 }
690
691 /***
692 * An internal helper class for storing information about contained
693 * configurations.
694 */
695 static class ConfigData
696 {
697 /*** Stores a reference to the configuration. */
698 private AbstractConfiguration configuration;
699
700 /*** Stores the name under which the configuration is stored. */
701 private String name;
702
703 /*** Stores the at information as path of nodes. */
704 private Collection atPath;
705
706 /*** Stores the at string.*/
707 private String at;
708
709 /*** Stores the root node for this child configuration.*/
710 private ConfigurationNode rootNode;
711
712 /***
713 * Creates a new instance of <code>ConfigData</code> and initializes
714 * it.
715 *
716 * @param config the configuration
717 * @param n the name
718 * @param at the at position
719 */
720 public ConfigData(AbstractConfiguration config, String n, String at)
721 {
722 configuration = config;
723 name = n;
724 atPath = parseAt(at);
725 this.at = at;
726 }
727
728 /***
729 * Returns the stored configuration.
730 *
731 * @return the configuration
732 */
733 public AbstractConfiguration getConfiguration()
734 {
735 return configuration;
736 }
737
738 /***
739 * Returns the configuration's name.
740 *
741 * @return the name
742 */
743 public String getName()
744 {
745 return name;
746 }
747
748 /***
749 * Returns the at position of this configuration.
750 *
751 * @return the at position
752 */
753 public String getAt()
754 {
755 return at;
756 }
757
758 /***
759 * Returns the root node for this child configuration.
760 *
761 * @return the root node of this child configuration
762 * @since 1.5
763 */
764 public ConfigurationNode getRootNode()
765 {
766 return rootNode;
767 }
768
769 /***
770 * Returns the transformed root node of the stored configuration. The
771 * term "transformed" means that an eventually defined at path
772 * has been applied.
773 *
774 * @return the transformed root node
775 */
776 public ConfigurationNode getTransformedRoot()
777 {
778 ViewNode result = new ViewNode();
779 ViewNode atParent = result;
780
781 if (atPath != null)
782 {
783
784 for (Iterator it = atPath.iterator(); it.hasNext();)
785 {
786 ViewNode node = new ViewNode();
787 node.setName((String) it.next());
788 atParent.addChild(node);
789 atParent = node;
790 }
791 }
792
793
794 HierarchicalConfiguration hc = ConfigurationUtils
795 .convertToHierarchical(getConfiguration());
796 atParent.appendChildren(hc.getRootNode());
797 atParent.appendAttributes(hc.getRootNode());
798 rootNode = hc.getRootNode();
799
800 return result;
801 }
802
803 /***
804 * Splits the at path into its components.
805 *
806 * @param at the at string
807 * @return a collection with the names of the single components
808 */
809 private Collection parseAt(String at)
810 {
811 if (at == null)
812 {
813 return null;
814 }
815
816 Collection result = new ArrayList();
817 DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(
818 AT_ENGINE, at).iterator();
819 while (it.hasNext())
820 {
821 result.add(it.nextKey());
822 }
823 return result;
824 }
825 }
826 }