XMLBeanDeclaration.java
- /*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.apache.commons.configuration2.beanutils;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.HashMap;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.function.Function;
- import java.util.stream.Collectors;
- import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
- import org.apache.commons.configuration2.HierarchicalConfiguration;
- import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
- import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
- import org.apache.commons.configuration2.tree.NodeHandler;
- import org.apache.commons.lang3.StringUtils;
- /**
- * <p>
- * An implementation of the {@code BeanDeclaration} interface that is suitable for XML configuration files.
- * </p>
- * <p>
- * This class defines the standard layout of a bean declaration in an XML configuration file. Such a declaration must
- * look like the following example fragment:
- * </p>
- *
- * <pre>
- * ...
- * <personBean config-class="my.model.PersonBean"
- * lastName="Doe" firstName="John">
- * <config-constrarg config-value="ID03493" config-type="java.lang.String"/>
- * <address config-class="my.model.AddressBean"
- * street="21st street 11" zip="1234"
- * city="TestCity"/>
- * </personBean>
- * </pre>
- *
- * <p>
- * The bean declaration can be contained in an arbitrary element. Here it is the {@code personBean} element. In the
- * attributes of this element there can occur some reserved attributes, which have the following meaning:
- * </p>
- * <dl>
- * <dt>{@code config-class}</dt>
- * <dd>Here the full qualified name of the bean's class can be specified. An instance of this class will be created. If
- * this attribute is not specified, the bean class must be provided in another way, for example as the {@code defaultClass}
- * passed to the {@code BeanHelper} class.</dd>
- * <dt>{@code config-factory}</dt>
- * <dd>This attribute can contain the name of the {@link BeanFactory} that should be used for creating the bean. If it
- * is defined, a factory with this name must have been registered at the {@code BeanHelper} class. If this attribute is
- * missing, the default bean factory will be used.</dd>
- * <dt>{@code config-factoryParam}</dt>
- * <dd>With this attribute a parameter can be specified that will be passed to the bean factory. This may be useful for
- * custom bean factories.</dd>
- * </dl>
- * <p>
- * All further attributes starting with the {@code config-} prefix are considered as meta data and will be ignored. All
- * other attributes are treated as properties of the bean to be created, i.e. corresponding setter methods of the bean
- * will be invoked with the values specified here.
- * </p>
- * <p>
- * If the bean to be created has also some complex properties (which are itself beans), their values cannot be
- * initialized from attributes. For this purpose nested elements can be used. The example listing shows how an address
- * bean can be initialized. This is done in a nested element whose name must match the name of a property of the
- * enclosing bean declaration. The format of this nested element is exactly the same as for the bean declaration itself,
- * i.e. it can have attributes defining meta data or bean properties and even further nested elements for complex bean
- * properties.
- * </p>
- * <p>
- * If the bean should be created using a specific constructor, the constructor arguments have to be specified. This is
- * done by an arbitrary number of nested {@code <config-constrarg>} elements. Each element can either have the
- * {@code config-value} attribute - then it defines a simple value - or must be again a bean declaration (conforming to
- * the format defined here) defining the complex value of this constructor argument.
- * </p>
- * <p>
- * A {@code XMLBeanDeclaration} object is usually created from a {@code HierarchicalConfiguration}. From this it will
- * derive a {@code SubnodeConfiguration}, which is used to access the needed properties. This subnode configuration can
- * be obtained using the {@link #getConfiguration()} method. All of its properties can be accessed in the usual way. To
- * ensure that the property keys used by this class are understood by the configuration, the default expression engine
- * will be set.
- * </p>
- *
- * @since 1.3
- */
- public class XMLBeanDeclaration implements BeanDeclaration {
- /**
- * An internal helper class which wraps the node with the bean declaration and the corresponding node handler.
- *
- * @param <T> the type of the node
- */
- static class NodeData<T> {
- /** The wrapped node. */
- private final T node;
- /** The node handler for interacting with this node. */
- private final NodeHandler<T> nodeHandler;
- /**
- * Constructs a new instance of {@code NodeData}.
- *
- * @param node the node
- * @param nodeHandler the node handler
- */
- NodeData(final T node, final NodeHandler<T> nodeHandler) {
- this.node = node;
- this.nodeHandler = nodeHandler;
- }
- /**
- * Returns the unescaped name of the node stored in this data object. This method handles the case that the node name
- * may contain reserved characters with a special meaning for the current expression engine. In this case, the
- * characters affected have to be escaped accordingly.
- *
- * @param config the configuration
- * @return the escaped node name
- */
- String escapedNodeName(final HierarchicalConfiguration<?> config) {
- return config.getExpressionEngine().nodeKey(node, StringUtils.EMPTY, nodeHandler);
- }
- /**
- * Gets the value of the attribute with the given name of the wrapped node.
- *
- * @param key the key of the attribute
- * @return the value of this attribute
- */
- Object getAttribute(final String key) {
- return nodeHandler.getAttributeValue(node, key);
- }
- /**
- * Gets a set with the names of the attributes of the wrapped node.
- *
- * @return the attribute names of this node
- */
- Set<String> getAttributes() {
- return nodeHandler.getAttributes(node);
- }
- /**
- * Gets a list with the children of the wrapped node, again wrapped into {@code NodeData} objects.
- *
- * @return a list with the children
- */
- List<NodeData<T>> getChildren() {
- return wrapInNodeData(nodeHandler.getChildren(node));
- }
- /**
- * Gets a list with the children of the wrapped node with the given name, again wrapped into {@code NodeData}
- * objects.
- *
- * @param name the name of the desired child nodes
- * @return a list with the children with this name
- */
- List<NodeData<T>> getChildren(final String name) {
- return wrapInNodeData(nodeHandler.getChildren(node, name));
- }
- /**
- * Returns a flag whether the wrapped node is the root node of the passed in configuration.
- *
- * @param config the configuration
- * @return a flag whether this node is the configuration's root node
- */
- boolean matchesConfigRootNode(final HierarchicalConfiguration<?> config) {
- return config.getNodeModel().getNodeHandler().getRootNode().equals(node);
- }
- /**
- * Returns the name of the wrapped node.
- *
- * @return the node name
- */
- String nodeName() {
- return nodeHandler.nodeName(node);
- }
- /**
- * Wraps the passed in list of nodes in {@code NodeData} objects.
- *
- * @param nodes the list with nodes
- * @return the wrapped nodes
- */
- List<NodeData<T>> wrapInNodeData(final List<T> nodes) {
- return nodes.stream().map(n -> new NodeData<>(n, nodeHandler)).collect(Collectors.toList());
- }
- }
- /** Constant for the prefix of reserved attributes. */
- public static final String RESERVED_PREFIX = "config-";
- /** Constant for the prefix for reserved attributes. */
- public static final String ATTR_PREFIX = "[@" + RESERVED_PREFIX;
- /** Constant for the bean class attribute. */
- public static final String ATTR_BEAN_CLASS = ATTR_PREFIX + "class]";
- /** Constant for the bean factory attribute. */
- public static final String ATTR_BEAN_FACTORY = ATTR_PREFIX + "factory]";
- /** Constant for the bean factory parameter attribute. */
- public static final String ATTR_FACTORY_PARAM = ATTR_PREFIX + "factoryParam]";
- /** Constant for the name of the bean class attribute. */
- private static final String ATTR_BEAN_CLASS_NAME = RESERVED_PREFIX + "class";
- /** Constant for the name of the element for constructor arguments. */
- private static final String ELEM_CTOR_ARG = RESERVED_PREFIX + "constrarg";
- /**
- * Constant for the name of the attribute with the value of a constructor argument.
- */
- private static final String ATTR_CTOR_VALUE = RESERVED_PREFIX + "value";
- /**
- * Constant for the name of the attribute with the data type of a constructor argument.
- */
- private static final String ATTR_CTOR_TYPE = RESERVED_PREFIX + "type";
- /**
- * Creates a {@code NodeData} object from the root node of the given configuration.
- *
- * @param config the configuration
- * @param <T> the type of the nodes
- * @return the {@code NodeData} object
- */
- private static <T> NodeData<T> createNodeDataFromConfiguration(final HierarchicalConfiguration<T> config) {
- final NodeHandler<T> handler = config.getNodeModel().getNodeHandler();
- return new NodeData<>(handler.getRootNode(), handler);
- }
- /**
- * Tests whether the constructor argument represented by the given configuration node is a bean declaration.
- *
- * @param nodeData the configuration node in question
- * @return a flag whether this constructor argument is a bean declaration
- */
- private static boolean isBeanDeclarationArgument(final NodeData<?> nodeData) {
- return !nodeData.getAttributes().contains(ATTR_BEAN_CLASS_NAME);
- }
- /** Stores the associated configuration. */
- private final HierarchicalConfiguration<?> configuration;
- /** Stores the configuration node that contains the bean declaration. */
- private final NodeData<?> nodeData;
- /** The name of the default bean class. */
- private final String defaultBeanClassName;
- /**
- * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it with the configuration node that contains the
- * bean declaration. This constructor is used internally.
- *
- * @param config the configuration
- * @param node the node with the bean declaration.
- */
- XMLBeanDeclaration(final HierarchicalConfiguration<?> config, final NodeData<?> node) {
- this.nodeData = node;
- configuration = config;
- defaultBeanClassName = null;
- initSubnodeConfiguration(config);
- }
- /**
- * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration. The
- * configuration's root node must contain the bean declaration.
- *
- * @param config the configuration with the bean declaration
- * @param <T> the node type of the configuration
- */
- public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config) {
- this(config, (String) null);
- }
- /**
- * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration. The passed in
- * key points to the bean declaration.
- *
- * @param config the configuration (must not be <strong>null</strong>)
- * @param key the key to the bean declaration (this key must point to exactly one bean declaration or a
- * {@code IllegalArgumentException} exception will be thrown)
- * @param <T> the node type of the configuration
- * @throws IllegalArgumentException if required information is missing to construct the bean declaration
- */
- public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key) {
- this(config, key, false);
- }
- /**
- * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration supporting
- * optional declarations.
- *
- * @param config the configuration (must not be <strong>null</strong>)
- * @param key the key to the bean declaration
- * @param optional a flag whether this declaration is optional; if set to <strong>true</strong>, no exception will be thrown if
- * the passed in key is undefined
- * @param <T> the node type of the configuration
- * @throws IllegalArgumentException if required information is missing to construct the bean declaration
- */
- public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key, final boolean optional) {
- this(config, key, optional, null);
- }
- /**
- * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration supporting
- * optional declarations and a default bean class name. The passed in key points to the bean declaration. If the key
- * does not exist and the boolean argument is <strong>true</strong>, the declaration is initialized with an empty configuration.
- * It is possible to create objects from such an empty declaration if a default class is provided. If the key on the
- * other hand has multiple values or is undefined and the boolean argument is <strong>false</strong>, a
- * {@code IllegalArgumentException} exception will be thrown. It is possible to set a default bean class name; this name
- * is used if the configuration does not contain a bean class.
- *
- * @param config the configuration (must not be <strong>null</strong>)
- * @param key the key to the bean declaration
- * @param optional a flag whether this declaration is optional; if set to <strong>true</strong>, no exception will be thrown if
- * the passed in key is undefined
- * @param defBeanClsName a default bean class name
- * @param <T> the node type of the configuration
- * @throws IllegalArgumentException if required information is missing to construct the bean declaration
- * @since 2.0
- */
- public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key, final boolean optional, final String defBeanClsName) {
- if (config == null) {
- throw new IllegalArgumentException("Configuration must not be null!");
- }
- HierarchicalConfiguration<?> tmpconfiguration;
- try {
- tmpconfiguration = config.configurationAt(key);
- } catch (final ConfigurationRuntimeException iex) {
- // If we reach this block, the key does not have exactly one value
- if (!optional || config.getMaxIndex(key) > 0) {
- throw iex;
- }
- tmpconfiguration = new BaseHierarchicalConfiguration();
- }
- this.nodeData = createNodeDataFromConfiguration(tmpconfiguration);
- this.configuration = tmpconfiguration;
- defaultBeanClassName = defBeanClsName;
- initSubnodeConfiguration(getConfiguration());
- }
- /**
- * Creates a new {@code BeanDeclaration} for a child node of the current configuration node. This method is called by
- * {@code getNestedBeanDeclarations()} for all complex sub properties detected by this method. Derived classes can hook
- * in if they need a specific initialization. This base implementation creates a {@code XMLBeanDeclaration} that is
- * properly initialized from the passed in node.
- *
- * @param nodeData the child node, for which a {@code BeanDeclaration} is to be created
- * @return the {@code BeanDeclaration} for this child node
- */
- BeanDeclaration createBeanDeclaration(final NodeData<?> nodeData) {
- for (final HierarchicalConfiguration<?> config : getConfiguration().configurationsAt(nodeData.escapedNodeName(getConfiguration()))) {
- if (nodeData.matchesConfigRootNode(config)) {
- return new XMLBeanDeclaration(config, nodeData);
- }
- }
- throw new ConfigurationRuntimeException("Unable to match node for " + nodeData.nodeName());
- }
- /**
- * Creates a {@code ConstructorArg} object for the specified configuration node.
- *
- * @param child the configuration node
- * @return the corresponding {@code ConstructorArg} object
- */
- private ConstructorArg createConstructorArg(final NodeData<?> child) {
- final String type = getAttribute(child, ATTR_CTOR_TYPE);
- if (isBeanDeclarationArgument(child)) {
- return ConstructorArg.forValue(getAttribute(child, ATTR_CTOR_VALUE), type);
- }
- return ConstructorArg.forBeanDeclaration(createBeanDeclaration(child), type);
- }
- /**
- * Gets an attribute of a configuration node. This method also takes interpolation into account.
- *
- * @param nodeData the node
- * @param attribute the name of the attribute
- * @return the string value of this attribute (can be <strong>null</strong>)
- */
- private String getAttribute(final NodeData<?> nodeData, final String attribute) {
- final Object value = nodeData.getAttribute(attribute);
- return value == null ? null : String.valueOf(interpolate(value));
- }
- /**
- * Gets a set with the names of the attributes of the configuration node holding the data of this bean declaration.
- *
- * @return the attribute names of the underlying configuration node
- */
- protected Set<String> getAttributeNames() {
- return getNode().getAttributes();
- }
- /**
- * Gets the name of the class of the bean to be created. This information is obtained from the {@code config-class}
- * attribute.
- *
- * @return the name of the bean's class
- */
- @Override
- public String getBeanClassName() {
- return getConfiguration().getString(ATTR_BEAN_CLASS, getDefaultBeanClassName());
- }
- /**
- * Gets the name of the bean factory. This information is fetched from the {@code config-factory} attribute.
- *
- * @return the name of the bean factory
- */
- @Override
- public String getBeanFactoryName() {
- return getConfiguration().getString(ATTR_BEAN_FACTORY, null);
- }
- /**
- * Gets a parameter for the bean factory. This information is fetched from the {@code config-factoryParam} attribute.
- *
- * @return the parameter for the bean factory
- */
- @Override
- public Object getBeanFactoryParameter() {
- return getConfiguration().getProperty(ATTR_FACTORY_PARAM);
- }
- /**
- * Gets a map with the bean's (simple) properties. The properties are collected from all attribute nodes, which are
- * not reserved.
- *
- * @return a map with the bean's properties
- */
- @Override
- public Map<String, Object> getBeanProperties() {
- return getAttributeNames().stream().filter(e -> !isReservedAttributeName(e))
- .collect(Collectors.toMap(Function.identity(), e -> interpolate(getNode().getAttribute(e))));
- }
- /**
- * Gets the configuration object this bean declaration is based on.
- *
- * @return the associated configuration
- */
- public HierarchicalConfiguration<?> getConfiguration() {
- return configuration;
- }
- /**
- * {@inheritDoc} This implementation processes all child nodes with the name {@code config-constrarg}. If such a node
- * has a {@code config-class} attribute, it is considered a nested bean declaration; otherwise it is interpreted as a
- * simple value. If no nested constructor argument declarations are found, result is an empty collection.
- */
- @Override
- public Collection<ConstructorArg> getConstructorArgs() {
- return getNode().getChildren(ELEM_CTOR_ARG).stream().map(this::createConstructorArg).collect(Collectors.toCollection(LinkedList::new));
- }
- /**
- * Gets the name of the default bean class. This class is used if no bean class is specified in the configuration. It
- * may be <strong>null</strong> if no default class was set.
- *
- * @return the default bean class name
- * @since 2.0
- */
- public String getDefaultBeanClassName() {
- return defaultBeanClassName;
- }
- /**
- * Gets a map with bean declarations for the complex properties of the bean to be created. These declarations are
- * obtained from the child nodes of this declaration's root node.
- *
- * @return a map with bean declarations for complex properties
- */
- @Override
- public Map<String, Object> getNestedBeanDeclarations() {
- final Map<String, Object> nested = new HashMap<>();
- getNode().getChildren().forEach(child -> {
- if (!isReservedChildName(child.nodeName())) {
- final Object obj = nested.get(child.nodeName());
- if (obj != null) {
- final List<BeanDeclaration> list;
- if (obj instanceof List) {
- // Safe because we created the lists ourselves.
- @SuppressWarnings("unchecked")
- final List<BeanDeclaration> tmpList = (List<BeanDeclaration>) obj;
- list = tmpList;
- } else {
- list = new ArrayList<>();
- list.add((BeanDeclaration) obj);
- nested.put(child.nodeName(), list);
- }
- list.add(createBeanDeclaration(child));
- } else {
- nested.put(child.nodeName(), createBeanDeclaration(child));
- }
- }
- });
- return nested;
- }
- /**
- * Gets the data about the associated node.
- *
- * @return the node with the bean declaration
- */
- NodeData<?> getNode() {
- return nodeData;
- }
- /**
- * Initializes the internally managed sub configuration. This method will set some default values for some properties.
- *
- * @param conf the configuration to initialize
- */
- private void initSubnodeConfiguration(final HierarchicalConfiguration<?> conf) {
- conf.setExpressionEngine(null);
- }
- /**
- * Performs interpolation for the specified value. This implementation will interpolate against the current subnode
- * configuration's parent. If sub classes need a different interpolation mechanism, they should override this method.
- *
- * @param value the value that is to be interpolated
- * @return the interpolated value
- */
- protected Object interpolate(final Object value) {
- final ConfigurationInterpolator interpolator = getConfiguration().getInterpolator();
- return interpolator != null ? interpolator.interpolate(value) : value;
- }
- /**
- * Tests if the specified attribute name is reserved and thus does not point to a property of the bean to be created.
- * This method is called when processing the attributes of this bean declaration. It is then possible to ignore some
- * attributes with a specific meaning. This implementation delegates to {@link #isReservedName(String)}.
- *
- * @param name the name of the attribute to be checked
- * @return a flag whether this name is reserved
- * @since 2.0
- */
- protected boolean isReservedAttributeName(final String name) {
- return isReservedName(name);
- }
- /**
- * Tests if the specified child node name is reserved and thus should be ignored. This method is called when processing
- * child nodes of this bean declaration. It is then possible to ignore some nodes with a specific meaning. This
- * implementation delegates to {@link #isReservedName(String)}.
- *
- * @param name the name of the child node to be checked
- * @return a flag whether this name is reserved
- * @since 2.0
- */
- protected boolean isReservedChildName(final String name) {
- return isReservedName(name);
- }
- /**
- * Tests if the specified name of a node or attribute is reserved and thus should be ignored. This method is called per
- * default by the methods for checking attribute and child node names. It checks whether the passed in name starts with
- * the reserved prefix.
- *
- * @param name the name to be checked
- * @return a flag whether this name is reserved
- */
- protected boolean isReservedName(final String name) {
- return name == null || name.startsWith(RESERVED_PREFIX);
- }
- }