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 }