    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 */
    017package org.apache.commons.configuration2.beanutils;
    018
    019import java.util.ArrayList;
    020import java.util.Collection;
    021import java.util.HashMap;
    022import java.util.LinkedList;
    023import java.util.List;
    024import java.util.Map;
    025import java.util.Set;
    026import java.util.stream.Collectors;
    027
    028import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
    029import org.apache.commons.configuration2.HierarchicalConfiguration;
    030import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
    031import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
    032import org.apache.commons.configuration2.tree.NodeHandler;
    033import org.apache.commons.lang3.StringUtils;
    034
    035/**
    036 * <p>
    037 * An implementation of the {@code BeanDeclaration} interface that is suitable for XML configuration files.
    038 * </p>
    039 * <p>
    040 * This class defines the standard layout of a bean declaration in an XML configuration file. Such a declaration must
    041 * look like the following example fragment:
    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;config-constrarg config-value=&quot;ID03493&quot; config-type=&quot;java.lang.String&quot;/&gt;
    049 *       &lt;address config-class=&quot;my.model.AddressBean&quot;
    050 *           street=&quot;21st street 11&quot; zip=&quot;1234&quot;
    051 *           city=&quot;TestCity&quot;/&gt;
    052 *   &lt;/personBean&gt;
    053 * </pre>
    054 *
    055 * <p>
    056 * The bean declaration can be contained in an arbitrary element. Here it is the {@code personBean} element. In the
    057 * attributes of this element there can occur some reserved attributes, which have the following meaning:
    058 * </p>
    059 * <dl>
    060 * <dt>{@code config-class}</dt>
    061 * <dd>Here the full qualified name of the bean's class can be specified. An instance of this class will be created. If
    062 * this attribute is not specified, the bean class must be provided in another way, e.g. as the {@code defaultClass}
    063 * passed to the {@code BeanHelper} class.</dd>
    064 * <dt>{@code config-factory}</dt>
    065 * <dd>This attribute can contain the name of the {@link BeanFactory} that should be used for creating the bean. If it
    066 * is defined, a factory with this name must have been registered at the {@code BeanHelper} class. If this attribute is
    067 * missing, the default bean factory will be used.</dd>
    068 * <dt>{@code config-factoryParam}</dt>
    069 * <dd>With this attribute a parameter can be specified that will be passed to the bean factory. This may be useful for
    070 * custom bean factories.</dd>
    071 * </dl>
    072 * <p>
    073 * All further attributes starting with the {@code config-} prefix are considered as meta data and will be ignored. All
    074 * other attributes are treated as properties of the bean to be created, i.e. corresponding setter methods of the bean
    075 * will be invoked with the values specified here.
    076 * </p>
    077 * <p>
    078 * If the bean to be created has also some complex properties (which are itself beans), their values cannot be
    079 * initialized from attributes. For this purpose nested elements can be used. The example listing shows how an address
    080 * bean can be initialized. This is done in a nested element whose name must match the name of a property of the
    081 * enclosing bean declaration. The format of this nested element is exactly the same as for the bean declaration itself,
    082 * i.e. it can have attributes defining meta data or bean properties and even further nested elements for complex bean
    083 * properties.
    084 * </p>
    085 * <p>
    086 * If the bean should be created using a specific constructor, the constructor arguments have to be specified. This is
    087 * done by an arbitrary number of nested {@code <config-constrarg>} elements. Each element can either have the
    088 * {@code config-value} attribute - then it defines a simple value - or must be again a bean declaration (conforming to
    089 * the format defined here) defining the complex value of this constructor argument.
    090 * </p>
    091 * <p>
    092 * A {@code XMLBeanDeclaration} object is usually created from a {@code HierarchicalConfiguration}. From this it will
    093 * derive a {@code SubnodeConfiguration}, which is used to access the needed properties. This subnode configuration can
    094 * be obtained using the {@link #getConfiguration()} method. All of its properties can be accessed in the usual way. To
    095 * ensure that the property keys used by this class are understood by the configuration, the default expression engine
    096 * will be set.
    097 * </p>
    098 *
    099 * @since 1.3
    100 */
    101public class XMLBeanDeclaration implements BeanDeclaration {
    102
    103    /** Constant for the prefix of reserved attributes. */
    104    public static final String RESERVED_PREFIX = "config-";
    105
    106    /** Constant for the prefix for reserved attributes. */
    107    public static final String ATTR_PREFIX = "[@" + RESERVED_PREFIX;
    108
    109    /** Constant for the bean class attribute. */
    110    public static final String ATTR_BEAN_CLASS = ATTR_PREFIX + "class]";
    111
    112    /** Constant for the bean factory attribute. */
    113    public static final String ATTR_BEAN_FACTORY = ATTR_PREFIX + "factory]";
    114
    115    /** Constant for the bean factory parameter attribute. */
    116    public static final String ATTR_FACTORY_PARAM = ATTR_PREFIX + "factoryParam]";
    117
    118    /** Constant for the name of the bean class attribute. */
    119    private static final String ATTR_BEAN_CLASS_NAME = RESERVED_PREFIX + "class";
    120
    121    /** Constant for the name of the element for constructor arguments. */
    122    private static final String ELEM_CTOR_ARG = RESERVED_PREFIX + "constrarg";
    123
    124    /**
    125     * Constant for the name of the attribute with the value of a constructor argument.
    126     */
    127    private static final String ATTR_CTOR_VALUE = RESERVED_PREFIX + "value";
    128
    129    /**
    130     * Constant for the name of the attribute with the data type of a constructor argument.
    131     */
    132    private static final String ATTR_CTOR_TYPE = RESERVED_PREFIX + "type";
    133
    134    /** Stores the associated configuration. */
    135    private final HierarchicalConfiguration<?> configuration;
    136
    137    /** Stores the configuration node that contains the bean declaration. */
    138    private final NodeData<?> nodeData;
    139
    140    /** The name of the default bean class. */
    141    private final String defaultBeanClassName;
    142
    143    /**
    144     * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration. The passed in
    145     * key points to the bean declaration.
    146     *
    147     * @param config the configuration (must not be <b>null</b>)
    148     * @param key the key to the bean declaration (this key must point to exactly one bean declaration or a
    149     *        {@code IllegalArgumentException} exception will be thrown)
    150     * @param <T> the node type of the configuration
    151     * @throws IllegalArgumentException if required information is missing to construct the bean declaration
    152     */
    153    public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key) {
    154        this(config, key, false);
    155    }
    156
    157    /**
    158     * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration supporting
    159     * optional declarations.
    160     *
    161     * @param config the configuration (must not be <b>null</b>)
    162     * @param key the key to the bean declaration
    163     * @param optional a flag whether this declaration is optional; if set to <b>true</b>, no exception will be thrown if
    164     *        the passed in key is undefined
    165     * @param <T> the node type of the configuration
    166     * @throws IllegalArgumentException if required information is missing to construct the bean declaration
    167     */
    168    public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key, final boolean optional) {
    169        this(config, key, optional, null);
    170    }
    171
    172    /**
    173     * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration supporting
    174     * optional declarations and a default bean class name. The passed in key points to the bean declaration. If the key
    175     * does not exist and the boolean argument is <b>true</b>, the declaration is initialized with an empty configuration.
    176     * It is possible to create objects from such an empty declaration if a default class is provided. If the key on the
    177     * other hand has multiple values or is undefined and the boolean argument is <b>false</b>, a
    178     * {@code IllegalArgumentException} exception will be thrown. It is possible to set a default bean class name; this name
    179     * is used if the configuration does not contain a bean class.
    180     *
    181     * @param config the configuration (must not be <b>null</b>)
    182     * @param key the key to the bean declaration
    183     * @param optional a flag whether this declaration is optional; if set to <b>true</b>, no exception will be thrown if
    184     *        the passed in key is undefined
    185     * @param defBeanClsName a default bean class name
    186     * @param <T> the node type of the configuration
    187     * @throws IllegalArgumentException if required information is missing to construct the bean declaration
    188     * @since 2.0
    189     */
    190    public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key, final boolean optional, final String defBeanClsName) {
    191        if (config == null) {
    192            throw new IllegalArgumentException("Configuration must not be null!");
    193        }
    194
    195        HierarchicalConfiguration<?> tmpconfiguration;
    196        try {
    197            tmpconfiguration = config.configurationAt(key);
    198        } catch (final ConfigurationRuntimeException iex) {
    199            // If we reach this block, the key does not have exactly one value
    200            if (!optional || config.getMaxIndex(key) > 0) {
    201                throw iex;
    202            }
    203            tmpconfiguration = new BaseHierarchicalConfiguration();
    204        }
    205        this.nodeData = createNodeDataFromConfiguration(tmpconfiguration);
    206        this.configuration = tmpconfiguration;
    207        defaultBeanClassName = defBeanClsName;
    208        initSubnodeConfiguration(getConfiguration());
    209    }
    210
    211    /**
    212     * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration. The
    213     * configuration's root node must contain the bean declaration.
    214     *
    215     * @param config the configuration with the bean declaration
    216     * @param <T> the node type of the configuration
    217     */
    218    public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config) {
    219        this(config, (String) null);
    220    }
    221
    222    /**
    223     * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it with the configuration node that contains the
    224     * bean declaration. This constructor is used internally.
    225     *
    226     * @param config the configuration
    227     * @param node the node with the bean declaration.
    228     */
    229    XMLBeanDeclaration(final HierarchicalConfiguration<?> config, final NodeData<?> node) {
    230        this.nodeData = node;
    231        configuration = config;
    232        defaultBeanClassName = null;
    233        initSubnodeConfiguration(config);
    234    }
    235
    236    /**
    237     * Gets the configuration object this bean declaration is based on.
    238     *
    239     * @return the associated configuration
    240     */
    241    public HierarchicalConfiguration<?> getConfiguration() {
    242        return configuration;
    243    }
    244
    245    /**
    246     * Gets the name of the default bean class. This class is used if no bean class is specified in the configuration. It
    247     * may be <b>null</b> if no default class was set.
    248     *
    249     * @return the default bean class name
    250     * @since 2.0
    251     */
    252    public String getDefaultBeanClassName() {
    253        return defaultBeanClassName;
    254    }
    255
    256    /**
    257     * Gets the name of the bean factory. This information is fetched from the {@code config-factory} attribute.
    258     *
    259     * @return the name of the bean factory
    260     */
    261    @Override
    262    public String getBeanFactoryName() {
    263        return getConfiguration().getString(ATTR_BEAN_FACTORY, null);
    264    }
    265
    266    /**
    267     * Gets a parameter for the bean factory. This information is fetched from the {@code config-factoryParam} attribute.
    268     *
    269     * @return the parameter for the bean factory
    270     */
    271    @Override
    272    public Object getBeanFactoryParameter() {
    273        return getConfiguration().getProperty(ATTR_FACTORY_PARAM);
    274    }
    275
    276    /**
    277     * Gets the name of the class of the bean to be created. This information is obtained from the {@code config-class}
    278     * attribute.
    279     *
    280     * @return the name of the bean's class
    281     */
    282    @Override
    283    public String getBeanClassName() {
    284        return getConfiguration().getString(ATTR_BEAN_CLASS, getDefaultBeanClassName());
    285    }
    286
    287    /**
    288     * Gets a map with the bean's (simple) properties. The properties are collected from all attribute nodes, which are
    289     * not reserved.
    290     *
    291     * @return a map with the bean's properties
    292     */
    293    @Override
    294    public Map<String, Object> getBeanProperties() {
    295        final Map<String, Object> props = new HashMap<>();
    296        for (final String key : getAttributeNames()) {
    297            if (!isReservedAttributeName(key)) {
    298                props.put(key, interpolate(getNode().getAttribute(key)));
    299            }
    300        }
    301
    302        return props;
    303    }
    304
    305    /**
    306     * Gets a map with bean declarations for the complex properties of the bean to be created. These declarations are
    307     * obtained from the child nodes of this declaration's root node.
    308     *
    309     * @return a map with bean declarations for complex properties
    310     */
    311    @Override
    312    public Map<String, Object> getNestedBeanDeclarations() {
    313        final Map<String, Object> nested = new HashMap<>();
    314        for (final NodeData<?> child : getNode().getChildren()) {
    315            if (!isReservedChildName(child.nodeName())) {
    316                if (nested.containsKey(child.nodeName())) {
    317                    final Object obj = nested.get(child.nodeName());
    318                    final List<BeanDeclaration> list;
    319                    if (obj instanceof List) {
    320                        // Safe because we created the lists ourselves.
    321                        @SuppressWarnings("unchecked")
    322                        final List<BeanDeclaration> tmpList = (List<BeanDeclaration>) obj;
    323                        list = tmpList;
    324                    } else {
    325                        list = new ArrayList<>();
    326                        list.add((BeanDeclaration) obj);
    327                        nested.put(child.nodeName(), list);
    328                    }
    329                    list.add(createBeanDeclaration(child));
    330                } else {
    331                    nested.put(child.nodeName(), createBeanDeclaration(child));
    332                }
    333            }
    334        }
    335
    336        return nested;
    337    }
    338
    339    /**
    340     * {@inheritDoc} This implementation processes all child nodes with the name {@code config-constrarg}. If such a node
    341     * has a {@code config-class} attribute, it is considered a nested bean declaration; otherwise it is interpreted as a
    342     * simple value. If no nested constructor argument declarations are found, result is an empty collection.
    343     */
    344    @Override
    345    public Collection<ConstructorArg> getConstructorArgs() {
    346        final Collection<ConstructorArg> args = new LinkedList<>();
    347        for (final NodeData<?> child : getNode().getChildren(ELEM_CTOR_ARG)) {
    348            args.add(createConstructorArg(child));
    349        }
    350        return args;
    351    }
    352
    353    /**
    354     * Performs interpolation for the specified value. This implementation will interpolate against the current subnode
    355     * configuration's parent. If sub classes need a different interpolation mechanism, they should override this method.
    356     *
    357     * @param value the value that is to be interpolated
    358     * @return the interpolated value
    359     */
    360    protected Object interpolate(final Object value) {
    361        final ConfigurationInterpolator interpolator = getConfiguration().getInterpolator();
    362        return interpolator != null ? interpolator.interpolate(value) : value;
    363    }
    364
    365    /**
    366     * Tests if the specified child node name is reserved and thus should be ignored. This method is called when processing
    367     * child nodes of this bean declaration. It is then possible to ignore some nodes with a specific meaning. This
    368     * implementation delegates to {@link #isReservedName(String)} .
    369     *
    370     * @param name the name of the child node to be checked
    371     * @return a flag whether this name is reserved
    372     * @since 2.0
    373     */
    374    protected boolean isReservedChildName(final String name) {
    375        return isReservedName(name);
    376    }
    377
    378    /**
    379     * Tests if the specified attribute name is reserved and thus does not point to a property of the bean to be created.
    380     * This method is called when processing the attributes of this bean declaration. It is then possible to ignore some
    381     * attributes with a specific meaning. This implementation delegates to {@link #isReservedName(String)}.
    382     *
    383     * @param name the name of the attribute to be checked
    384     * @return a flag whether this name is reserved
    385     * @since 2.0
    386     */
    387    protected boolean isReservedAttributeName(final String name) {
    388        return isReservedName(name);
    389    }
    390
    391    /**
    392     * Tests if the specified name of a node or attribute is reserved and thus should be ignored. This method is called per
    393     * default by the methods for checking attribute and child node names. It checks whether the passed in name starts with
    394     * the reserved prefix.
    395     *
    396     * @param name the name to be checked
    397     * @return a flag whether this name is reserved
    398     */
    399    protected boolean isReservedName(final String name) {
    400        return name == null || name.startsWith(RESERVED_PREFIX);
    401    }
    402
    403    /**
    404     * Gets a set with the names of the attributes of the configuration node holding the data of this bean declaration.
    405     *
    406     * @return the attribute names of the underlying configuration node
    407     */
    408    protected Set<String> getAttributeNames() {
    409        return getNode().getAttributes();
    410    }
    411
    412    /**
    413     * Gets the data about the associated node.
    414     *
    415     * @return the node with the bean declaration
    416     */
    417    NodeData<?> getNode() {
    418        return nodeData;
    419    }
    420
    421    /**
    422     * Creates a new {@code BeanDeclaration} for a child node of the current configuration node. This method is called by
    423     * {@code getNestedBeanDeclarations()} for all complex sub properties detected by this method. Derived classes can hook
    424     * in if they need a specific initialization. This base implementation creates a {@code XMLBeanDeclaration} that is
    425     * properly initialized from the passed in node.
    426     *
    427     * @param nodeData the child node, for which a {@code BeanDeclaration} is to be created
    428     * @return the {@code BeanDeclaration} for this child node
    429     */
    430    BeanDeclaration createBeanDeclaration(final NodeData<?> nodeData) {
    431        for (final HierarchicalConfiguration<?> config : getConfiguration().configurationsAt(nodeData.escapedNodeName(getConfiguration()))) {
    432            if (nodeData.matchesConfigRootNode(config)) {
    433                return new XMLBeanDeclaration(config, nodeData);
    434            }
    435        }
    436        throw new ConfigurationRuntimeException("Unable to match node for " + nodeData.nodeName());
    437    }
    438
    439    /**
    440     * Initializes the internally managed sub configuration. This method will set some default values for some properties.
    441     *
    442     * @param conf the configuration to initialize
    443     */
    444    private void initSubnodeConfiguration(final HierarchicalConfiguration<?> conf) {
    445        conf.setExpressionEngine(null);
    446    }
    447
    448    /**
    449     * Creates a {@code ConstructorArg} object for the specified configuration node.
    450     *
    451     * @param child the configuration node
    452     * @return the corresponding {@code ConstructorArg} object
    453     */
    454    private ConstructorArg createConstructorArg(final NodeData<?> child) {
    455        final String type = getAttribute(child, ATTR_CTOR_TYPE);
    456        if (isBeanDeclarationArgument(child)) {
    457            return ConstructorArg.forValue(getAttribute(child, ATTR_CTOR_VALUE), type);
    458        }
    459        return ConstructorArg.forBeanDeclaration(createBeanDeclaration(child), type);
    460    }
    461
    462    /**
    463     * Gets an attribute of a configuration node. This method also takes interpolation into account.
    464     *
    465     * @param nodeData the node
    466     * @param attribute the name of the attribute
    467     * @return the string value of this attribute (can be <b>null</b>)
    468     */
    469    private String getAttribute(final NodeData<?> nodeData, final String attribute) {
    470        final Object value = nodeData.getAttribute(attribute);
    471        return value == null ? null : String.valueOf(interpolate(value));
    472    }
    473
    474    /**
    475     * Tests whether the constructor argument represented by the given configuration node is a bean declaration.
    476     *
    477     * @param nodeData the configuration node in question
    478     * @return a flag whether this constructor argument is a bean declaration
    479     */
    480    private static boolean isBeanDeclarationArgument(final NodeData<?> nodeData) {
    481        return !nodeData.getAttributes().contains(ATTR_BEAN_CLASS_NAME);
    482    }
    483
    484    /**
    485     * Creates a {@code NodeData} object from the root node of the given configuration.
    486     *
    487     * @param config the configuration
    488     * @param <T> the type of the nodes
    489     * @return the {@code NodeData} object
    490     */
    491    private static <T> NodeData<T> createNodeDataFromConfiguration(final HierarchicalConfiguration<T> config) {
    492        final NodeHandler<T> handler = config.getNodeModel().getNodeHandler();
    493        return new NodeData<>(handler.getRootNode(), handler);
    494    }
    495
    496    /**
    497     * An internal helper class which wraps the node with the bean declaration and the corresponding node handler.
    498     *
    499     * @param <T> the type of the node
    500     */
    501    static class NodeData<T> {
    502
    503        /** The wrapped node. */
    504        private final T node;
    505
    506        /** The node handler for interacting with this node. */
    507        private final NodeHandler<T> nodeHandler;
    508
    509        /**
    510         * Constructs a new instance of {@code NodeData}.
    511         *
    512         * @param node the node
    513         * @param nodeHandler the node handler
    514         */
    515        NodeData(final T node, final NodeHandler<T> nodeHandler) {
    516            this.node = node;
    517            this.nodeHandler = nodeHandler;
    518        }
    519
    520        /**
    521         * Returns the name of the wrapped node.
    522         *
    523         * @return the node name
    524         */
    525        String nodeName() {
    526            return nodeHandler.nodeName(node);
    527        }
    528
    529        /**
    530         * Returns the unescaped name of the node stored in this data object. This method handles the case that the node name
    531         * may contain reserved characters with a special meaning for the current expression engine. In this case, the
    532         * characters affected have to be escaped accordingly.
    533         *
    534         * @param config the configuration
    535         * @return the escaped node name
    536         */
    537        String escapedNodeName(final HierarchicalConfiguration<?> config) {
    538            return config.getExpressionEngine().nodeKey(node, StringUtils.EMPTY, nodeHandler);
    539        }
    540
    541        /**
    542         * Gets a list with the children of the wrapped node, again wrapped into {@code NodeData} objects.
    543         *
    544         * @return a list with the children
    545         */
    546        List<NodeData<T>> getChildren() {
    547            return wrapInNodeData(nodeHandler.getChildren(node));
    548        }
    549
    550        /**
    551         * Gets a list with the children of the wrapped node with the given name, again wrapped into {@code NodeData}
    552         * objects.
    553         *
    554         * @param name the name of the desired child nodes
    555         * @return a list with the children with this name
    556         */
    557        List<NodeData<T>> getChildren(final String name) {
    558            return wrapInNodeData(nodeHandler.getChildren(node, name));
    559        }
    560
    561        /**
    562         * Gets a set with the names of the attributes of the wrapped node.
    563         *
    564         * @return the attribute names of this node
    565         */
    566        Set<String> getAttributes() {
    567            return nodeHandler.getAttributes(node);
    568        }
    569
    570        /**
    571         * Gets the value of the attribute with the given name of the wrapped node.
    572         *
    573         * @param key the key of the attribute
    574         * @return the value of this attribute
    575         */
    576        Object getAttribute(final String key) {
    577            return nodeHandler.getAttributeValue(node, key);
    578        }
    579
    580        /**
    581         * Returns a flag whether the wrapped node is the root node of the passed in configuration.
    582         *
    583         * @param config the configuration
    584         * @return a flag whether this node is the configuration's root node
    585         */
    586        boolean matchesConfigRootNode(final HierarchicalConfiguration<?> config) {
    587            return config.getNodeModel().getNodeHandler().getRootNode().equals(node);
    588        }
    589
    590        /**
    591         * Wraps the passed in list of nodes in {@code NodeData} objects.
    592         *
    593         * @param nodes the list with nodes
    594         * @return the wrapped nodes
    595         */
    596        List<NodeData<T>> wrapInNodeData(final List<T> nodes) {
    597            return nodes.stream().map(n -> new NodeData<>(n, nodeHandler)).collect(Collectors.toList());
    598        }
    599    }
    600}