    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *     http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017    package org.apache.commons.configuration.beanutils;
    018
    019    import java.util.HashMap;
    020    import java.util.Iterator;
    021    import java.util.Map;
    022    import java.util.List;
    023    import java.util.ArrayList;
    024
    025    import org.apache.commons.configuration.HierarchicalConfiguration;
    026    import org.apache.commons.configuration.PropertyConverter;
    027    import org.apache.commons.configuration.SubnodeConfiguration;
    028    import org.apache.commons.configuration.ConfigurationRuntimeException;
    029    import org.apache.commons.configuration.tree.ConfigurationNode;
    030    import org.apache.commons.configuration.tree.DefaultConfigurationNode;
    031
    032    /**
    033     * <p>
    034     * An implementation of the <code>BeanDeclaration</code> interface that is
    035     * suitable for XML configuration files.
    036     * </p>
    037     * <p>
    038     * This class defines the standard layout of a bean declaration in an XML
    039     * configuration file. Such a declaration must look like the following example
    040     * fragement:
    041     * </p>
    042     * <p>
    043     *
    044     * <pre>
    045     *   ...
    046     *   &lt;personBean config-class=&quot;my.model.PersonBean&quot;
    047     *       lastName=&quot;Doe&quot; firstName=&quot;John&quot;&gt;
    048     *       &lt;address config-class=&quot;my.model.AddressBean&quot;
    049     *           street=&quot;21st street 11&quot; zip=&quot;1234&quot;
    050     *           city=&quot;TestCity&quot;/&gt;
    051     *   &lt;/personBean&gt;
    052     * </pre>
    053     *
    054     * </p>
    055     * <p>
    056     * The bean declaration can be contained in an arbitrary element. Here it is the
    057     * <code>&lt;personBean&gt;</code> element. In the attributes of this element
    058     * there can occur some reserved attributes, which have the following meaning:
    059     * <dl>
    060     * <dt><code>config-class</code></dt>
    061     * <dd>Here the full qualified name of the bean's class can be specified. An
    062     * instance of this class will be created. If this attribute is not specified,
    063     * the bean class must be provided in another way, e.g. as the
    064     * <code>defaultClass</code> passed to the <code>BeanHelper</code> class.</dd>
    065     * <dt><code>config-factory</code></dt>
    066     * <dd>This attribute can contain the name of the
    067     * <code>{@link BeanFactory}</code> that should be used for creating the bean.
    068     * If it is defined, a factory with this name must have been registered at the
    069     * <code>BeanHelper</code> class. If this attribute is missing, the default
    070     * bean factory will be used.</dd>
    071     * <dt><code>config-factoryParam</code></dt>
    072     * <dd>With this attribute a parameter can be specified that will be passed to
    073     * the bean factory. This may be useful for custom bean factories.</dd>
    074     * </dl>
    075     * </p>
    076     * <p>
    077     * All further attributes starting with the <code>config-</code> prefix are
    078     * considered as meta data and will be ignored. All other attributes are treated
    079     * as properties of the bean to be created, i.e. corresponding setter methods of
    080     * the bean will be invoked with the values specified here.
    081     * </p>
    082     * <p>
    083     * If the bean to be created has also some complex properties (which are itself
    084     * beans), their values cannot be initialized from attributes. For this purpose
    085     * nested elements can be used. The example listing shows how an address bean
    086     * can be initialized. This is done in a nested element whose name must match
    087     * the name of a property of the enclosing bean declaration. The format of this
    088     * nested element is exactly the same as for the bean declaration itself, i.e.
    089     * it can have attributes defining meta data or bean properties and even further
    090     * nested elements for complex bean properties.
    091     * </p>
    092     * <p>
    093     * A <code>XMLBeanDeclaration</code> object is usually created from a
    094     * <code>HierarchicalConfiguration</code>. From this it will derive a
    095     * <code>SubnodeConfiguration</code>, which is used to access the needed
    096     * properties. This subnode configuration can be obtained using the
    097     * <code>{@link #getConfiguration()}</code> method. All of its properties can
    098     * be accessed in the usual way. To ensure that the property keys used by this
    099     * class are understood by the configuration, the default expression engine will
    100     * be set.
    101     * </p>
    102     *
    103     * @since 1.3
    104     * @author Oliver Heger
    105     * @version $Id: XMLBeanDeclaration.java 766914 2009-04-20 23:38:49Z rgoers $
    106     */
    107    public class XMLBeanDeclaration implements BeanDeclaration
    108    {
    109        /** Constant for the prefix of reserved attributes. */
    110        public static final String RESERVED_PREFIX = "config-";
    111
    112        /** Constant for the prefix for reserved attributes.*/
    113        public static final String ATTR_PREFIX = "[@" + RESERVED_PREFIX;
    114
    115        /** Constant for the bean class attribute. */
    116        public static final String ATTR_BEAN_CLASS = ATTR_PREFIX + "class]";
    117
    118        /** Constant for the bean factory attribute. */
    119        public static final String ATTR_BEAN_FACTORY = ATTR_PREFIX + "factory]";
    120
    121        /** Constant for the bean factory parameter attribute. */
    122        public static final String ATTR_FACTORY_PARAM = ATTR_PREFIX
    123                + "factoryParam]";
    124
    125        /** Stores the associated configuration. */
    126        private SubnodeConfiguration configuration;
    127
    128        /** Stores the configuration node that contains the bean declaration. */
    129        private ConfigurationNode node;
    130
    131        /**
    132         * Creates a new instance of <code>XMLBeanDeclaration</code> and
    133         * initializes it from the given configuration. The passed in key points to
    134         * the bean declaration.
    135         *
    136         * @param config the configuration
    137         * @param key the key to the bean declaration (this key must point to
    138         * exactly one bean declaration or a <code>IllegalArgumentException</code>
    139         * exception will be thrown)
    140         */
    141        public XMLBeanDeclaration(HierarchicalConfiguration config, String key)
    142        {
    143            this(config, key, false);
    144        }
    145
    146        /**
    147         * Creates a new instance of <code>XMLBeanDeclaration</code> and
    148         * initializes it from the given configuration. The passed in key points to
    149         * the bean declaration. If the key does not exist and the boolean argument
    150         * is <b>true</b>, the declaration is initialized with an empty
    151         * configuration. It is possible to create objects from such an empty
    152         * declaration if a default class is provided. If the key on the other hand
    153         * has multiple values or is undefined and the boolean argument is <b>false</b>,
    154         * a <code>IllegalArgumentException</code> exception will be thrown.
    155         *
    156         * @param config the configuration
    157         * @param key the key to the bean declaration
    158         * @param optional a flag whether this declaration is optional; if set to
    159         * <b>true</b>, no exception will be thrown if the passed in key is
    160         * undefined
    161         */
    162        public XMLBeanDeclaration(HierarchicalConfiguration config, String key,
    163                boolean optional)
    164        {
    165            if (config == null)
    166            {
    167                throw new IllegalArgumentException(
    168                        "Configuration must not be null!");
    169            }
    170
    171            try
    172            {
    173                configuration = config.configurationAt(key);
    174                node = configuration.getRootNode();
    175            }
    176            catch (IllegalArgumentException iex)
    177            {
    178                // If we reach this block, the key does not have exactly one value
    179                if (!optional || config.getMaxIndex(key) > 0)
    180                {
    181                    throw iex;
    182                }
    183                configuration = config.configurationAt(null);
    184                node = new DefaultConfigurationNode();
    185            }
    186            initSubnodeConfiguration(getConfiguration());
    187        }
    188
    189        /**
    190         * Creates a new instance of <code>XMLBeanDeclaration</code> and
    191         * initializes it from the given configuration. The configuration's root
    192         * node must contain the bean declaration.
    193         *
    194         * @param config the configuration with the bean declaration
    195         */
    196        public XMLBeanDeclaration(HierarchicalConfiguration config)
    197        {
    198            this(config, (String) null);
    199        }
    200
    201        /**
    202         * Creates a new instance of <code>XMLBeanDeclaration</code> and
    203         * initializes it with the configuration node that contains the bean
    204         * declaration.
    205         *
    206         * @param config the configuration
    207         * @param node the node with the bean declaration.
    208         */
    209        public XMLBeanDeclaration(SubnodeConfiguration config,
    210                ConfigurationNode node)
    211        {
    212            if (config == null)
    213            {
    214                throw new IllegalArgumentException(
    215                        "Configuration must not be null!");
    216            }
    217            if (node == null)
    218            {
    219                throw new IllegalArgumentException("Node must not be null!");
    220            }
    221
    222            this.node = node;
    223            configuration = config;
    224            initSubnodeConfiguration(config);
    225        }
    226
    227        /**
    228         * Returns the configuration object this bean declaration is based on.
    229         *
    230         * @return the associated configuration
    231         */
    232        public SubnodeConfiguration getConfiguration()
    233        {
    234            return configuration;
    235        }
    236
    237        /**
    238         * Returns the node that contains the bean declaration.
    239         *
    240         * @return the configuration node this bean declaration is based on
    241         */
    242        public ConfigurationNode getNode()
    243        {
    244            return node;
    245        }
    246
    247        /**
    248         * Returns the name of the bean factory. This information is fetched from
    249         * the <code>config-factory</code> attribute.
    250         *
    251         * @return the name of the bean factory
    252         */
    253        public String getBeanFactoryName()
    254        {
    255            return getConfiguration().getString(ATTR_BEAN_FACTORY);
    256        }
    257
    258        /**
    259         * Returns a parameter for the bean factory. This information is fetched
    260         * from the <code>config-factoryParam</code> attribute.
    261         *
    262         * @return the parameter for the bean factory
    263         */
    264        public Object getBeanFactoryParameter()
    265        {
    266            return getConfiguration().getProperty(ATTR_FACTORY_PARAM);
    267        }
    268
    269        /**
    270         * Returns the name of the class of the bean to be created. This information
    271         * is obtained from the <code>config-class</code> attribute.
    272         *
    273         * @return the name of the bean's class
    274         */
    275        public String getBeanClassName()
    276        {
    277            return getConfiguration().getString(ATTR_BEAN_CLASS);
    278        }
    279
    280        /**
    281         * Returns a map with the bean's (simple) properties. The properties are
    282         * collected from all attribute nodes, which are not reserved.
    283         *
    284         * @return a map with the bean's properties
    285         */
    286        public Map getBeanProperties()
    287        {
    288            Map props = new HashMap();
    289            for (Iterator it = getNode().getAttributes().iterator(); it.hasNext();)
    290            {
    291                ConfigurationNode attr = (ConfigurationNode) it.next();
    292                if (!isReservedNode(attr))
    293                {
    294                    props.put(attr.getName(), interpolate(attr .getValue()));
    295                }
    296            }
    297
    298            return props;
    299        }
    300
    301        /**
    302         * Returns a map with bean declarations for the complex properties of the
    303         * bean to be created. These declarations are obtained from the child nodes
    304         * of this declaration's root node.
    305         *
    306         * @return a map with bean declarations for complex properties
    307         */
    308        public Map getNestedBeanDeclarations()
    309        {
    310            Map nested = new HashMap();
    311            for (Iterator it = getNode().getChildren().iterator(); it.hasNext();)
    312            {
    313                ConfigurationNode child = (ConfigurationNode) it.next();
    314                if (!isReservedNode(child))
    315                {
    316                    if (nested.containsKey(child.getName()))
    317                    {
    318                        Object obj = nested.get(child.getName());
    319                        List list;
    320                        if (obj instanceof List)
    321                        {
    322                            list = (List) obj;
    323                        }
    324                        else
    325                        {
    326                            list = new ArrayList();
    327                            list.add(obj);
    328                            nested.put(child.getName(), list);
    329                        }
    330                        list.add(createBeanDeclaration(child));
    331                    }
    332                    else
    333                    {
    334                        nested.put(child.getName(), createBeanDeclaration(child));
    335                    }
    336                }
    337            }
    338
    339            return nested;
    340        }
    341
    342        /**
    343         * Performs interpolation for the specified value. This implementation will
    344         * interpolate against the current subnode configuration's parent. If sub
    345         * classes need a different interpolation mechanism, they should override
    346         * this method.
    347         *
    348         * @param value the value that is to be interpolated
    349         * @return the interpolated value
    350         */
    351        protected Object interpolate(Object value)
    352        {
    353            return PropertyConverter.interpolate(value, getConfiguration()
    354                    .getParent());
    355        }
    356
    357        /**
    358         * Checks if the specified node is reserved and thus should be ignored. This
    359         * method is called when the maps for the bean's properties and complex
    360         * properties are collected. It checks whether the given node is an
    361         * attribute node and if its name starts with the reserved prefix.
    362         *
    363         * @param nd the node to be checked
    364         * @return a flag whether this node is reserved (and does not point to a
    365         * property)
    366         */
    367        protected boolean isReservedNode(ConfigurationNode nd)
    368        {
    369            return nd.isAttribute()
    370                    && (nd.getName() == null || nd.getName().startsWith(
    371                            RESERVED_PREFIX));
    372        }
    373
    374        /**
    375         * Creates a new <code>BeanDeclaration</code> for a child node of the
    376         * current configuration node. This method is called by
    377         * <code>getNestedBeanDeclarations()</code> for all complex sub properties
    378         * detected by this method. Derived classes can hook in if they need a
    379         * specific initialization. This base implementation creates a
    380         * <code>XMLBeanDeclaration</code> that is properly initialized from the
    381         * passed in node.
    382         *
    383         * @param node the child node, for which a <code>BeanDeclaration</code> is
    384         *        to be created
    385         * @return the <code>BeanDeclaration</code> for this child node
    386         * @since 1.6
    387         */
    388        protected BeanDeclaration createBeanDeclaration(ConfigurationNode node)
    389        {
    390            List list = getConfiguration().configurationsAt(node.getName());
    391            if (list.size() == 1)
    392            {
    393                return new XMLBeanDeclaration((SubnodeConfiguration) list.get(0), node);
    394            }
    395            else
    396            {
    397                Iterator iter = list.iterator();
    398                while (iter.hasNext())
    399                {
    400                    SubnodeConfiguration config = (SubnodeConfiguration) iter.next();
    401                    if (config.getRootNode().equals(node))
    402                    {
    403                        return new XMLBeanDeclaration(config, node);
    404                    }
    405                }
    406                throw new ConfigurationRuntimeException("Unable to match node for " + node.getName());
    407            }
    408        }
    409
    410        /**
    411         * Initializes the internally managed subnode configuration. This method
    412         * will set some default values for some properties.
    413         *
    414         * @param conf the configuration to initialize
    415         */
    416        private void initSubnodeConfiguration(SubnodeConfiguration conf)
    417        {
    418            conf.setThrowExceptionOnMissing(false);
    419            conf.setExpressionEngine(null);
    420        }
    421    }