/*
 * $Id: FormBeanConfig.java 472728 2006-11-09 01:10:58Z niallp $
 *
 * 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.struts.config;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.action.ActionForm;
import org.apache.struts.chain.commands.util.ClassUtils;
import org.apache.struts.util.ResponseUtils;


/**
 * <p>A JavaBean representing the configuration information of a
 * <code>&lt;form-bean&gt;</code> element in a Struts configuration file.<p>
 *
 * @version $Rev: 472728 $ $Date: 2006-01-17 07:26:20 -0500 (Tue, 17 Jan 2006)
 *          $
 * @since Struts 1.1
 */
public class FormBeanConfig extends BaseConfig {

    /** serialVersionUID */
    private static final long serialVersionUID = 3515968502450681140L;

    /** LOGGER */
    private static final Log LOG = LogFactory.getLog(FormBeanConfig.class);

    // ----------------------------------------------------- Instance Variables

    /**
     * The set of FormProperty elements defining dynamic form properties for
     * this form bean, keyed by property name.
     */
    private final Map<String, FormPropertyConfig> formProperties = new HashMap<>();

    // ------------------------------------------------------------- Properties

    /**
     * The name of the FormBeanConfig that this config inherits configuration
     * information from.
     */
    private String inherit = null;

    /**
     * Have the inheritance values for this class been applied?
     */
    private boolean extensionProcessed = false;

    /**
     * The unique identifier of this form bean, which is used to reference
     * this bean in <code>ActionMapping</code> instances as well as for the
     * name of the request or session attribute under which the corresponding
     * form bean instance is created or accessed.
     */
    private String name = null;

    /**
     * The fully qualified Java class name of the implementation class to be
     * used or generated.
     */
    private String type = null;

    /**
     * Is this DynaClass currently restricted (for DynaBeans with a
     * MutableDynaClass).
     */
    private boolean restricted = false;

    /**
     * @return Extends
     */
    public String getExtends() {
        return this.inherit;
    }

    /**
     * @param extend Extends
     */
    public void setExtends(final String extend) {
        throwIfConfigured();
        this.inherit = extend;
    }

    /**
     * @return boolean
     */
    public boolean isExtensionProcessed() {
        return this.extensionProcessed;
    }

    /**
     * @return Name
     */
    public String getName() {
        return this.name;
    }

    /**
     * @param val Name
     */
    public void setName(final String val) {
        throwIfConfigured();
        this.name = val;
    }

    /**
     * @return Type
     */
    public String getType() {
        return this.type;
    }

    /**
     * @param val Type
     */
    public void setType(final String val) {
        throwIfConfigured();
        this.type = val;
    }

    /**
     * <p>Indicates whether a MutableDynaClass is currently restricted.</p>
     * <p>If so, no changes to the existing registration of property names,
     * data types, readability, or writeability are allowed.</p>
     * @return boolean
     */
    public boolean isRestricted() {
        return this.restricted;
    }

    /**
     * <p>Set whether a MutableDynaClass is currently restricted.</p> <p>If
     * so, no changes to the existing registration of property names, data
     * types, readability, or writeability are allowed.</p>
     * @param val boolean
     */
    public void setRestricted(final boolean val) {
        this.restricted = val;
    }

    // ------------------------------------------------------ Protected Methods

    /**
     * <p>Traces the hierarchy of this object to check if any of the ancestors
     * is extending this instance.</p>
     *
     * @param moduleConfig The configuration for the module being configured.
     * @return true if circular inheritance was detected.
     */
    protected boolean checkCircularInheritance(final Map<String, FormBeanConfig> moduleConfig) {
        String ancestorName = getExtends();

        while (ancestorName != null) {
            // check if we have the same name as an ancestor
            if (getName().equals(ancestorName)) {
                return true;
            }

            // get our ancestor's ancestor
            FormBeanConfig ancestor =
                moduleConfig.get(ancestorName);

            ancestorName = ancestor.getExtends();
        }

        return false;
    }

    /**
     * <p>Compare the form properties of this bean with that of the given and
     * copy those that are not present.</p>
     * @param config The form bean config to copy properties from.
     * @see #inheritFrom(FormBeanConfig)
     */
    protected void inheritFormProperties(final FormBeanConfig config) {

        throwIfConfigured();

        // Inherit form property configs

        for (FormPropertyConfig baseFpc : config.findFormPropertyConfigs()) {
            // Do we have this prop?
            FormPropertyConfig prop =
                findFormPropertyConfig(baseFpc.getName());

            if (prop == null) {
                try {
                    // We don't have this, so let's copy it
                    prop = ResponseUtils.applicationInstance(baseFpc.getClass().getName());

                    BeanUtils.copyProperties(prop, baseFpc);

                } catch (final ReflectiveOperationException e) {
                    LOG.warn(e.getMessage());
                    throw new RuntimeException(e);
                }

                addFormPropertyConfig(prop);
                prop.setProperties(baseFpc.copyProperties());
            }
        }
    }

    // --------------------------------------------------------- Public Methods

    /**
     * <p>Create and return an <code>ActionForm</code> instance appropriate to
     * the information in this <code>FormBeanConfig</code>.</p>
     *
     * <p>Although this method is not formally deprecated yet, where possible,
     * the form which accepts an <code>ActionContext</code> as an argument is
     * preferred, to help sever direct dependencies on the Servlet API.  As
     * the ActionContext becomes more familiar in Struts, this method will
     * almost certainly be deprecated.</p>
     *
     *
     * @return ActionForm instance
     */
    public ActionForm createActionForm() {

        try {
            // Create a new form bean instance
            Object obj = formBeanClass().getDeclaredConstructor().newInstance();
            return (ActionForm) obj;
        } catch (final ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * <p>Checks if the given <code>ActionForm</code> instance is suitable for
     * use as an alternative to calling this <code>FormBeanConfig</code>
     * instance's <code>createActionForm</code> method.</p>
     *
     * @param form an existing form instance that may be reused.
     * @return true if the given form can be reused as the form for this
     *         config.
     */
    public boolean canReuse(final ActionForm form) {
        if (form != null) {
            // check if the form's class is compatible with the class
            //      we're configured for
            Class<?> formClass = form.getClass();

            Class<?> configClass =
                ClassUtils.getApplicationClass(getType());

            if (configClass.isAssignableFrom(formClass)) {
                LOG.debug("Can reuse existing instance (non-dynamic)");
                return true;
            }
        }

        return false;
    }

    /**
     * Add a new <code>FormPropertyConfig</code> instance to the set
     * associated with this module.
     *
     * @param config The new configuration instance to be added
     * @throws IllegalArgumentException if this property name has already been
     *                                  defined
     */
    public void addFormPropertyConfig(final FormPropertyConfig config) {
        throwIfConfigured();

        if (this.formProperties.containsKey(config.getName())) {
            throw new IllegalArgumentException("Property " + config.getName()
                + " already defined");
        }

        this.formProperties.put(config.getName(), config);
    }

    /**
     * Return the form property configuration for the specified property name,
     * if any; otherwise return <code>null</code>.
     * @param val Form property name to find a configuration for
     * @return FormPropertyConfig
     */
    public FormPropertyConfig findFormPropertyConfig(final String val) {
        return this.formProperties.get(val);
    }

    /**
     * Return the form property configurations for this module.  If there are
     * none, a zero-length array is returned.
     * @return FormPropertyConfig
     */
    public FormPropertyConfig[] findFormPropertyConfigs() {
        FormPropertyConfig[] results = new FormPropertyConfig[this.formProperties.size()];

        return this.formProperties.values().toArray(results);
    }

    /**
     * Freeze the configuration of this component.
     */
    @Override
    public void freeze() {
        super.freeze();

        for (FormPropertyConfig config : findFormPropertyConfigs()) {
            config.freeze();
        }
    }

    /**
     * <p>Inherit values that have not been overridden from the provided
     * config object.  Subclasses overriding this method should verify that
     * the given parameter is of a class that contains a property it is trying
     * to inherit:</p>
     * <pre>
     * if (config instanceof MyCustomConfig) {
     *     MyCustomConfig myConfig =
     *         (MyCustomConfig) config;
     *
     *     if (getMyCustomProp() == null) {
     *         setMyCustomProp(myConfig.getMyCustomProp());
     *     }
     * }
     * </pre>
     * <p>If the given <code>config</code> is extending another object, those
     * extensions should be resolved before it's used as a parameter to this
     * method.</p>
     * @param config The object that this instance will be inheriting its
     *               values from.
     *
     */
    public void inheritFrom(final FormBeanConfig config) {

        throwIfConfigured();

        // Inherit values that have not been overridden
        if (getName() == null) {
            setName(config.getName());
        }

        if (!isRestricted()) {
            setRestricted(config.isRestricted());
        }

        if (getType() == null) {
            setType(config.getType());
        }

        inheritFormProperties(config);
        inheritProperties(config);
    }

    /**
     * <p>Inherit configuration information from the FormBeanConfig that this
     * instance is extending.  This method verifies that any form bean config
     * object that it inherits from has also had its processExtends() method
     * called.</p>
     * @param moduleConfig The {@link ModuleConfig} that this bean is from.
     * @see #inheritFrom(FormBeanConfig)
     */
    public void processExtends(final Map<String, FormBeanConfig> moduleConfig) {

        super.throwIfConfigured();

        String ancestor = getExtends();

        if (!this.extensionProcessed && (ancestor != null)) {
            FormBeanConfig baseConfig =
                moduleConfig.get(ancestor);

            if (baseConfig == null) {
                throw new NullPointerException("Unable to find "
                    + "form bean '" + ancestor + "' to extend.");
            }

            // Check against circle inheritance and make sure the base config's
            //  own extends have been processed already
            if (checkCircularInheritance(moduleConfig)) {
                throw new IllegalArgumentException(
                    "Circular inheritance detected for form bean " + getName());
            }

            // Make sure the ancestor's own extension has been processed.
            if (!baseConfig.isExtensionProcessed()) {
                baseConfig.processExtends(moduleConfig);
            }

            // Copy values from the base config
            inheritFrom(baseConfig);
        }

        this.extensionProcessed = true;
    }

    /**
     * Remove the specified form property configuration instance.
     *
     * @param config FormPropertyConfig instance to be removed
     */
    public void removeFormPropertyConfig(final FormPropertyConfig config) {
        super.throwIfConfigured();

        this.formProperties.remove(config.getName());
    }

    /**
     * Return a String representation of this object.
     */
    @Override
    public String toString() {
        final String sb = "FormBeanConfig["
                + "name=" + this.name
                + ",type=" + this.type
                + ",extends=" + this.inherit
                + "]";
        return sb;
    }

    // ------------------------------------------------------ Protected Methods
    /**
     * Return the <code>Class</code> instance for the form bean implementation
     * configured by this <code>FormBeanConfig</code> instance.  This method
     * uses the same algorithm as <code>RequestUtils.applicationClass()</code>
     * but is reproduced to avoid a runtime dependence.
     * @return Class
     */
    protected Class<?> formBeanClass() {
        ClassLoader classLoader =
            Thread.currentThread().getContextClassLoader();

        if (classLoader == null) {
            classLoader = this.getClass().getClassLoader();
        }

        try {
            return classLoader.loadClass(getType());
        } catch (final ReflectiveOperationException e) {
            LOG.warn(e.getMessage());
            return null;
        }
    }
}
