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 truely 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 add 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. Node that, depending on the used
64 * <code>NodeCombiner</code>, this may be a complex operation.
65 * </p>
66 * <p>
67 * It is not strictly forbidden to manipulate a
68 * <code>CombinedConfiguration</code> directly, but the results may be
69 * unpredictable. For instance some node combiners use special view nodes for
70 * linking parts of the original configurations' data together. If new
71 * properties are added to such a special node, they do not belong to any of the
72 * managed configurations and thus hang in the air. It is also possible that
73 * direct updates on a <code>CombinedConfiguration</code> are incompatible
74 * with the used node combiner (e.g. if the
75 * <code>{@link org.apache.commons.configuration.tree.OverrideCombiner OverrideCombiner}</code>
76 * is used and properties are removed the resulting node structure may be
77 * incorrect because some properties that were hidden by the removed properties
78 * are not visible). So it is recommended to perform updates only on the managed
79 * configurations.
80 * </p>
81 * <p>
82 * Whenever the node structure of a <code>CombinedConfiguration</code> becomes
83 * invalid (either because one of the contained configurations was modified or
84 * because the <code>invalidate()</code> method was directly called) an event
85 * is generated. So this can be detected by interested event listeners. This
86 * also makes it possible to add a combined configuration into another one.
87 * </p>
88 * <p>
89 * Implementation note: Adding and removing configurations to and from a
90 * combined configuration is not thread-safe. If a combined configuration is
91 * manipulated by multiple threads, the developer has to take care about
92 * properly synchronization.
93 * </p>
94 *
95 * @author <a
96 * href="http://jakarta.apache.org/commons/configuration/team-list.html">Commons
97 * Configuration team</a>
98 * @since 1.3
99 * @version $Id: CombinedConfiguration.java 439648 2006-09-02 20:42:10Z oheger $
100 */
101 public class CombinedConfiguration extends HierarchicalConfiguration implements
102 ConfigurationListener, Cloneable
103 {
104 /***
105 * Constant for the invalidate event that is fired when the internal node
106 * structure becomes invalid.
107 */
108 public static final int EVENT_COMBINED_INVALIDATE = 40;
109
110 /***
111 * The serial version ID.
112 */
113 private static final long serialVersionUID = 8338574525528692307L;
114
115 /*** Constant for the expression engine for parsing the at path. */
116 private static final DefaultExpressionEngine AT_ENGINE = new DefaultExpressionEngine();
117
118 /*** Constant for the default node combiner. */
119 private static final NodeCombiner DEFAULT_COMBINER = new UnionCombiner();
120
121 /*** Stores the combiner. */
122 private NodeCombiner nodeCombiner;
123
124 /*** Stores the combined root node. */
125 private ConfigurationNode combinedRoot;
126
127 /*** Stores a list with the contained configurations. */
128 private List configurations;
129
130 /*** Stores a map with the named configurations. */
131 private Map namedConfigurations;
132
133 /***
134 * Creates a new instance of <code>CombinedConfiguration</code> and
135 * initializes the combiner to be used.
136 *
137 * @param comb the node combiner (can be <b>null</b>, then a union combiner
138 * is used as default)
139 */
140 public CombinedConfiguration(NodeCombiner comb)
141 {
142 setNodeCombiner((comb != null) ? comb : DEFAULT_COMBINER);
143 clear();
144 }
145
146 /***
147 * Creates a new instance of <code>CombinedConfiguration</code> that uses
148 * a union combiner.
149 *
150 * @see org.apache.commons.configuration.tree.UnionCombiner
151 */
152 public CombinedConfiguration()
153 {
154 this(null);
155 }
156
157 /***
158 * Returns the node combiner that is used for creating the combined node
159 * structure.
160 *
161 * @return the node combiner
162 */
163 public NodeCombiner getNodeCombiner()
164 {
165 return nodeCombiner;
166 }
167
168 /***
169 * Sets the node combiner. This object will be used when the combined node
170 * structure is to be constructed. It must not be <b>null</b>, otherwise an
171 * <code>IllegalArgumentException</code> exception is thrown. Changing the
172 * node combiner causes an invalidation of this combined configuration, so
173 * that the new combiner immediately takes effect.
174 *
175 * @param nodeCombiner the node combiner
176 */
177 public void setNodeCombiner(NodeCombiner nodeCombiner)
178 {
179 if (nodeCombiner == null)
180 {
181 throw new IllegalArgumentException(
182 "Node combiner must not be null!");
183 }
184 this.nodeCombiner = nodeCombiner;
185 invalidate();
186 }
187
188 /***
189 * Adds a new configuration to this combined configuration. It is possible
190 * (but not mandatory) to give the new configuration a name. This name must
191 * be unique, otherwise a <code>ConfigurationRuntimeException</code> will
192 * be thrown. With the optional <code>at</code> argument you can specify
193 * where in the resulting node structure the content of the added
194 * configuration should appear. This is a string that uses dots as property
195 * delimiters (independent on the current expression engine). For instance
196 * if you pass in the string <code>"database.tables"</code>,
197 * all properties of the added configuration will occur in this branch.
198 *
199 * @param config the configuration to add (must not be <b>null</b>)
200 * @param name the name of this configuration (can be <b>null</b>)
201 * @param at the position of this configuration in the combined tree (can be
202 * <b>null</b>)
203 */
204 public void addConfiguration(AbstractConfiguration config, String name,
205 String at)
206 {
207 if (config == null)
208 {
209 throw new IllegalArgumentException(
210 "Added configuration must not be null!");
211 }
212 if (name != null && namedConfigurations.containsKey(name))
213 {
214 throw new ConfigurationRuntimeException(
215 "A configuration with the name '"
216 + name
217 + "' already exists in this combined configuration!");
218 }
219
220 ConfigData cd = new ConfigData(config, name, at);
221 configurations.add(cd);
222 if (name != null)
223 {
224 namedConfigurations.put(name, config);
225 }
226
227 config.addConfigurationListener(this);
228 invalidate();
229 }
230
231 /***
232 * Adds a new configuration to this combined configuration with an optional
233 * name. The new configuration's properties will be added under the root of
234 * the combined node structure.
235 *
236 * @param config the configuration to add (must not be <b>null</b>)
237 * @param name the name of this configuration (can be <b>null</b>)
238 */
239 public void addConfiguration(AbstractConfiguration config, String name)
240 {
241 addConfiguration(config, name, null);
242 }
243
244 /***
245 * Adds a new configuration to this combined configuration. The new
246 * configuration is not given a name. Its properties will be added under the
247 * root of the combined node structure.
248 *
249 * @param config the configuration to add (must not be <b>null</b>)
250 */
251 public void addConfiguration(AbstractConfiguration config)
252 {
253 addConfiguration(config, null, null);
254 }
255
256 /***
257 * Returns the number of configurations that are contained in this combined
258 * configuration.
259 *
260 * @return the number of contained configurations
261 */
262 public int getNumberOfConfigurations()
263 {
264 return configurations.size();
265 }
266
267 /***
268 * Returns the configuration at the specified index. The contained
269 * configurations are numbered in the order they were added to this combined
270 * configuration. The index of the first configuration is 0.
271 *
272 * @param index the index
273 * @return the configuration at this index
274 */
275 public Configuration getConfiguration(int index)
276 {
277 ConfigData cd = (ConfigData) configurations.get(index);
278 return cd.getConfiguration();
279 }
280
281 /***
282 * Returns the configuration with the given name. This can be <b>null</b>
283 * if no such configuration exists.
284 *
285 * @param name the name of the configuration
286 * @return the configuration with this name
287 */
288 public Configuration getConfiguration(String name)
289 {
290 return (Configuration) namedConfigurations.get(name);
291 }
292
293 /***
294 * Removes the specified configuration from this combined configuration.
295 *
296 * @param config the configuration to be removed
297 * @return a flag whether this configuration was found and could be removed
298 */
299 public boolean removeConfiguration(Configuration config)
300 {
301 for (int index = 0; index < getNumberOfConfigurations(); index++)
302 {
303 if (((ConfigData) configurations.get(index)).getConfiguration() == config)
304 {
305 removeConfigurationAt(index);
306 return true;
307 }
308 }
309
310 return false;
311 }
312
313 /***
314 * Removes the configuration at the specified index.
315 *
316 * @param index the index
317 * @return the removed configuration
318 */
319 public Configuration removeConfigurationAt(int index)
320 {
321 ConfigData cd = (ConfigData) configurations.remove(index);
322 if (cd.getName() != null)
323 {
324 namedConfigurations.remove(cd.getName());
325 }
326 cd.getConfiguration().removeConfigurationListener(this);
327 invalidate();
328 return cd.getConfiguration();
329 }
330
331 /***
332 * Removes the configuration with the specified name.
333 *
334 * @param name the name of the configuration to be removed
335 * @return the removed configuration (<b>null</b> if this configuration
336 * was not found)
337 */
338 public Configuration removeConfiguration(String name)
339 {
340 Configuration conf = getConfiguration(name);
341 if (conf != null)
342 {
343 removeConfiguration(conf);
344 }
345 return conf;
346 }
347
348 /***
349 * Returns a set with the names of all configurations contained in this
350 * combined configuration. Of course here are only these configurations
351 * listed, for which a name was specified when they were added.
352 *
353 * @return a set with the names of the contained configurations (never
354 * <b>null</b>)
355 */
356 public Set getConfigurationNames()
357 {
358 return namedConfigurations.keySet();
359 }
360
361 /***
362 * Invalidates this combined configuration. This means that the next time a
363 * property is accessed the combined node structure must be re-constructed.
364 * Invalidation of a combined configuration also means that an event of type
365 * <code>EVENT_COMBINED_INVALIDATE</code> is fired. Note that while other
366 * events most times appear twice (once before and once after an update),
367 * this event is only fired once (after update).
368 */
369 public void invalidate()
370 {
371 synchronized (getNodeCombiner())
372 {
373 combinedRoot = null;
374 }
375 fireEvent(EVENT_COMBINED_INVALIDATE, null, null, false);
376 }
377
378 /***
379 * Event listener call back for configuration update events. This method is
380 * called whenever one of the contained configurations was modified. It
381 * invalidates this combined configuration.
382 *
383 * @param event the update event
384 */
385 public void configurationChanged(ConfigurationEvent event)
386 {
387 invalidate();
388 }
389
390 /***
391 * Returns the configuration root node of this combined configuration. This
392 * method will construct a combined node structure using the current node
393 * combiner if necessary.
394 *
395 * @return the combined root node
396 */
397 public ConfigurationNode getRootNode()
398 {
399 synchronized (getNodeCombiner())
400 {
401 if (combinedRoot == null)
402 {
403 combinedRoot = constructCombinedNode();
404 }
405 return combinedRoot;
406 }
407 }
408
409 /***
410 * Clears this configuration. All contained configurations will be removed.
411 */
412 public void clear()
413 {
414 fireEvent(EVENT_CLEAR, null, null, true);
415 configurations = new ArrayList();
416 namedConfigurations = new HashMap();
417 fireEvent(EVENT_CLEAR, null, null, false);
418 invalidate();
419 }
420
421 /***
422 * Returns a copy of this object. This implementation performs a deep clone,
423 * i.e. all contained configurations will be cloned, too. For this to work,
424 * all contained configurations must be cloneable. Registered event
425 * listeners won't be cloned. The clone will use the same node combiner than
426 * the original.
427 *
428 * @return the copied object
429 */
430 public Object clone()
431 {
432 CombinedConfiguration copy = (CombinedConfiguration) super.clone();
433 copy.clear();
434 for (Iterator it = configurations.iterator(); it.hasNext();)
435 {
436 ConfigData cd = (ConfigData) it.next();
437 copy.addConfiguration((AbstractConfiguration) ConfigurationUtils
438 .cloneConfiguration(cd.getConfiguration()), cd.getName(),
439 cd.getAt());
440 }
441
442 copy.setRootNode(new DefaultConfigurationNode());
443 return copy;
444 }
445
446 /***
447 * Creates the root node of this combined configuration.
448 *
449 * @return the combined root node
450 */
451 private ConfigurationNode constructCombinedNode()
452 {
453 if (getNumberOfConfigurations() < 1)
454 {
455 return new ViewNode();
456 }
457
458 else
459 {
460 Iterator it = configurations.iterator();
461 ConfigurationNode node = ((ConfigData) it.next())
462 .getTransformedRoot();
463 while (it.hasNext())
464 {
465 node = getNodeCombiner().combine(node,
466 ((ConfigData) it.next()).getTransformedRoot());
467 }
468 return node;
469 }
470 }
471
472 /***
473 * An internal helper class for storing information about contained
474 * configurations.
475 */
476 static class ConfigData
477 {
478 /*** Stores a reference to the configuration. */
479 private AbstractConfiguration configuration;
480
481 /*** Stores the name under which the configuration is stored. */
482 private String name;
483
484 /*** Stores the at information as path of nodes. */
485 private Collection atPath;
486
487 /*** Stores the at string.*/
488 private String at;
489
490 /***
491 * Creates a new instance of <code>ConfigData</code> and initializes
492 * it.
493 *
494 * @param config the configuration
495 * @param n the name
496 * @param at the at position
497 */
498 public ConfigData(AbstractConfiguration config, String n, String at)
499 {
500 configuration = config;
501 name = n;
502 atPath = parseAt(at);
503 this.at = at;
504 }
505
506 /***
507 * Returns the stored configuration.
508 *
509 * @return the configuration
510 */
511 public AbstractConfiguration getConfiguration()
512 {
513 return configuration;
514 }
515
516 /***
517 * Returns the configuration's name.
518 *
519 * @return the name
520 */
521 public String getName()
522 {
523 return name;
524 }
525
526 /***
527 * Returns the at position of this configuration.
528 *
529 * @return the at position
530 */
531 public String getAt()
532 {
533 return at;
534 }
535
536 /***
537 * Returns the transformed root node of the stored configuration. The
538 * term "transformed" means that an eventually defined at path
539 * has been applied.
540 *
541 * @return the transformed root node
542 */
543 public ConfigurationNode getTransformedRoot()
544 {
545 ViewNode result = new ViewNode();
546 ViewNode atParent = result;
547
548 if (atPath != null)
549 {
550
551 for (Iterator it = atPath.iterator(); it.hasNext();)
552 {
553 ViewNode node = new ViewNode();
554 node.setName((String) it.next());
555 atParent.addChild(node);
556 atParent = node;
557 }
558 }
559
560
561 HierarchicalConfiguration hc = ConfigurationUtils
562 .convertToHierarchical(getConfiguration());
563 atParent.appendChildren(hc.getRootNode());
564 atParent.appendAttributes(hc.getRootNode());
565
566 return result;
567 }
568
569 /***
570 * Splits the at path into its components.
571 *
572 * @param at the at string
573 * @return a collection with the names of the single components
574 */
575 private Collection parseAt(String at)
576 {
577 if (at == null)
578 {
579 return null;
580 }
581
582 Collection result = new ArrayList();
583 DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(
584 AT_ENGINE, at).iterator();
585 while (it.hasNext())
586 {
587 result.add(it.nextKey());
588 }
589 return result;
590 }
591 }
592 }