View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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>&quot;database.tables&quot;</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()) // use combiner as lock
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 &quot;transformed&quot; 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                 // Build the complete path
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             // Copy data of the root node to the new path
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 }