/*
 * 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 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>
 *   ...
 *   &lt;personBean config-class=&quot;my.model.PersonBean&quot;
 *       lastName=&quot;Doe&quot; firstName=&quot;John&quot;&gt;
 *       &lt;config-constrarg config-value=&quot;ID03493&quot; config-type=&quot;java.lang.String&quot;/&gt;
 *       &lt;address config-class=&quot;my.model.AddressBean&quot;
 *           street=&quot;21st street 11&quot; zip=&quot;1234&quot;
 *           city=&quot;TestCity&quot;/&gt;
 *   &lt;/personBean&gt;
 * </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, e.g. 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
 * @version $Id: XMLBeanDeclaration.java 1790899 2017-04-10 21:56:46Z ggregory $
 */
public class XMLBeanDeclaration implements BeanDeclaration
{
    /** 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";

    /** Stores the associated configuration. */
    private final HierarchicalConfiguration<?> configuration;

    /** Stores the configuration node that contains the bean declaration. */
    private final NodeData<?> node;

    /** The name of the default bean class. */
    private final String defaultBeanClassName;

    /**
     * Creates 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 <b>null</b>)
     * @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(HierarchicalConfiguration<T> config, String key)
    {
        this(config, key, false);
    }

    /**
     * Creates a new instance of {@code XMLBeanDeclaration} and initializes it
     * from the given configuration supporting optional declarations.
     *
     * @param config the configuration (must not be <b>null</b>)
     * @param key the key to the bean declaration
     * @param optional a flag whether this declaration is optional; if set to
     *        <b>true</b>, 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(HierarchicalConfiguration<T> config, String key,
            boolean optional)
    {
        this(config, key, optional, null);
    }

    /**
     * Creates 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
     * <b>true</b>, 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 <b>false</b>, 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 <b>null</b>)
     * @param key the key to the bean declaration
     * @param optional a flag whether this declaration is optional; if set to
     *        <b>true</b>, 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(HierarchicalConfiguration<T> config, String key,
            boolean optional, String defBeanClsName)
    {
        if (config == null)
        {
            throw new IllegalArgumentException(
                    "Configuration must not be null!");
        }

        HierarchicalConfiguration<?> tmpconfiguration;
        try
        {
            tmpconfiguration = config.configurationAt(key);
        }
        catch (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.node = createNodeDataFromConfiguration(tmpconfiguration);
        this.configuration = tmpconfiguration;
        defaultBeanClassName = defBeanClsName;
        initSubnodeConfiguration(getConfiguration());
    }

    /**
     * Creates 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(HierarchicalConfiguration<T> config)
    {
        this(config, (String) null);
    }

    /**
     * Creates 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(HierarchicalConfiguration<?> config,
            NodeData<?> node)
    {
        this.node = node;
        configuration = config;
        defaultBeanClassName = null;
        initSubnodeConfiguration(config);
    }

    /**
     * Returns the configuration object this bean declaration is based on.
     *
     * @return the associated configuration
     */
    public HierarchicalConfiguration<?> getConfiguration()
    {
        return configuration;
    }

    /**
     * Returns the name of the default bean class. This class is used if no bean
     * class is specified in the configuration. It may be <b>null</b> if no
     * default class was set.
     *
     * @return the default bean class name
     * @since 2.0
     */
    public String getDefaultBeanClassName()
    {
        return defaultBeanClassName;
    }

    /**
     * Returns 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);
    }

    /**
     * Returns 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);
    }

    /**
     * Returns 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());
    }

    /**
     * Returns 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()
    {
        Map<String, Object> props = new HashMap<>();
        for (String key : getAttributeNames())
        {
            if (!isReservedAttributeName(key))
            {
                props.put(key, interpolate(getNode().getAttribute(key)));
            }
        }

        return props;
    }

    /**
     * Returns 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()
    {
        Map<String, Object> nested = new HashMap<>();
        for (NodeData<?> child : getNode().getChildren())
        {
            if (!isReservedChildName(child.nodeName()))
            {
                if (nested.containsKey(child.nodeName()))
                {
                    Object obj = nested.get(child.nodeName());
                    List<BeanDeclaration> list;
                    if (obj instanceof List)
                    {
                        // Safe because we created the lists ourselves.
                        @SuppressWarnings("unchecked")
                        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;
    }

    /**
     * {@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()
    {
        Collection<ConstructorArg> args = new LinkedList<>();
        for (NodeData<?> child : getNode().getChildren(ELEM_CTOR_ARG))
        {
            args.add(createConstructorArg(child));
        }
        return args;
    }

    /**
     * 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(Object value)
    {
        ConfigurationInterpolator interpolator =
                getConfiguration().getInterpolator();
        return (interpolator != null) ? interpolator.interpolate(value) : value;
    }

    /**
     * Checks 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(String name)
    {
        return isReservedName(name);
    }

    /**
     * Checks 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(String name)
    {
        return isReservedName(name);
    }

    /**
     * Checks 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(String name)
    {
        return name == null || name.startsWith(RESERVED_PREFIX);
    }

    /**
     * Returns 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();
    }

    /**
     * Returns the data about the associated node.
     *
     * @return the node with the bean declaration
     */
    NodeData<?> getNode()
    {
        return node;
    }

    /**
     * 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 node the child node, for which a {@code BeanDeclaration} is
     *        to be created
     * @return the {@code BeanDeclaration} for this child node
     */
    BeanDeclaration createBeanDeclaration(NodeData<?> node)
    {
        for (HierarchicalConfiguration<?> config : getConfiguration()
                .configurationsAt(node.escapedNodeName(getConfiguration())))
        {
            if (node.matchesConfigRootNode(config))
            {
                return new XMLBeanDeclaration(config, node);
            }
        }
        throw new ConfigurationRuntimeException("Unable to match node for "
                + node.nodeName());
    }

    /**
     * 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(HierarchicalConfiguration<?> conf)
    {
        conf.setExpressionEngine(null);
    }

    /**
     * Creates a {@code ConstructorArg} object for the specified configuration
     * node.
     *
     * @param child the configuration node
     * @return the corresponding {@code ConstructorArg} object
     */
    private ConstructorArg createConstructorArg(NodeData<?> child)
    {
        String type = getAttribute(child, ATTR_CTOR_TYPE);
        if (isBeanDeclarationArgument(child))
        {
            return ConstructorArg.forValue(
                    getAttribute(child, ATTR_CTOR_VALUE), type);
        }
        else
        {
            return ConstructorArg.forBeanDeclaration(
                    createBeanDeclaration(child), type);
        }
    }

    /**
     * Helper method for obtaining an attribute of a configuration node.
     * This method also takes interpolation into account.
     *
     * @param nd the node
     * @param attr the name of the attribute
     * @return the string value of this attribute (can be <b>null</b>)
     */
    private String getAttribute(NodeData<?> nd, String attr)
    {
        Object value = nd.getAttribute(attr);
        return (value == null) ? null : String.valueOf(interpolate(value));
    }

    /**
     * Checks whether the constructor argument represented by the given
     * configuration node is a bean declaration.
     *
     * @param nd the configuration node in question
     * @return a flag whether this constructor argument is a bean declaration
     */
    private static boolean isBeanDeclarationArgument(NodeData<?> nd)
    {
        return !nd.getAttributes().contains(ATTR_BEAN_CLASS_NAME);
    }

    /**
     * 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(
            HierarchicalConfiguration<T> config)
    {
        NodeHandler<T> handler = config.getNodeModel().getNodeHandler();
        return new NodeData<>(handler.getRootNode(), handler);
    }

    /**
     * An internally used 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> handler;

        /**
         * Creates a new instance of {@code NodeData}.
         *
         * @param nd the node
         * @param hndlr the handler
         */
        public NodeData(T nd, NodeHandler<T> hndlr)
        {
            node = nd;
            handler = hndlr;
        }

        /**
         * Returns the name of the wrapped node.
         *
         * @return the node name
         */
        public String nodeName()
        {
            return handler.nodeName(node);
        }

        /**
         * 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
         */
        public String escapedNodeName(HierarchicalConfiguration<?> config)
        {
            return config.getExpressionEngine().nodeKey(node,
                    StringUtils.EMPTY, handler);
        }

        /**
         * Returns a list with the children of the wrapped node, again wrapped
         * into {@code NodeData} objects.
         *
         * @return a list with the children
         */
        public List<NodeData<T>> getChildren()
        {
            return wrapInNodeData(handler.getChildren(node));
        }

        /**
         * Returns 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
         */
        public List<NodeData<T>> getChildren(String name)
        {
            return wrapInNodeData(handler.getChildren(node, name));
        }

        /**
         * Returns a set with the names of the attributes of the wrapped node.
         *
         * @return the attribute names of this node
         */
        public Set<String> getAttributes()
        {
            return handler.getAttributes(node);
        }

        /**
         * Returns 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
         */
        public Object getAttribute(String key)
        {
            return handler.getAttributeValue(node, key);
        }

        /**
         * 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
         */
        public boolean matchesConfigRootNode(HierarchicalConfiguration<?> config)
        {
            return config.getNodeModel().getNodeHandler().getRootNode()
                    .equals(node);
        }

        /**
         * Wraps the passed in list of nodes in {@code NodeData} objects.
         *
         * @param nodes the list with nodes
         * @return the wrapped nodes
         */
        private List<NodeData<T>> wrapInNodeData(List<T> nodes)
        {
            List<NodeData<T>> result = new ArrayList<>(nodes.size());
            for (T node : nodes)
            {
                result.add(new NodeData<>(node, handler));
            }
            return result;
        }
    }
}
