/*
 * $Id: ActionConfig.java 480593 2006-11-29 15:17:52Z 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.ArrayList;
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.util.ResponseUtils;


/**
 * <p>A JavaBean representing the configuration information of an
 * <code>&lt;action&gt;</code> element from a Struts module configuration
 * file.</p>
 *
 * @version $Rev: 480593 $ $Date: 2006-11-29 09:17:52 -0600 (Wed, 29 Nov 2006) $
 * @since Struts 1.1
 */
public class ActionConfig extends BaseConfig {

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

    /** log */
    private static final Log LOG = LogFactory.getLog(ActionConfig.class);

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

    /**
     * <p> The set of exception handling configurations for this action, if
     * any, keyed by the <code>type</code> property. </p>
     */
    private final Map<String, ExceptionConfig> exceptions = new HashMap<>();

    /**
     * <p> The set of local forward configurations for this action, if any,
     * keyed by the <code>name</code> property. </p>
     */
    private final Map<String, ForwardConfig> forwards = new HashMap<>();

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

    /**
     * <p> The module configuration with which we are associated. </p>
     */
    private ModuleConfig moduleConfig = null;

    /**
     * <p> The request-scope or session-scope attribute name under which our
     * form bean is accessed, if it is different from the form bean's
     * specified <code>name</code>. </p>
     */
    private String attribute = null;

    /**
     * <p>The internal identification of this action mapping. Identifications are
     * not inheritable and must be unique within a module.</p>
     *
     * @since Struts 1.3.6
     */
    private String actionId = null;

    /**
     * <p>The path of the ActionConfig that this object should inherit
     * properties from.</p> </p>
     */
    private String inherit = null;

    /**
     * Indicates whether the "cancellable " property has been set or not.
     */
    private boolean cancellableSet = false;

    /**
     * <p>Can this Action be cancelled? [false]</p> <p> By default, when an
     * Action is cancelled, validation is bypassed and the Action should not
     * execute the business operation. If a request tries to cancel an Action
     * when cancellable is not set, a "InvalidCancelException" is thrown.
     * </p>
     */
    private boolean cancellable = false;

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

    /**
     * <p> Context-relative path of the web application resource that will
     * process this request via RequestDispatcher.forward(), instead of
     * instantiating and calling the <code>Action</code> class specified by
     * "type". Exactly one of <code>forward</code>, <code>include</code>, or
     * <code>type</code> must be specified. </p>
     */
    private String forward = null;

    /**
     * <p> Context-relative path of the web application resource that will
     * process this request via RequestDispatcher.include(), instead of
     * instantiating and calling the <code>Action</code> class specified by
     * "type". Exactly one of <code>forward</code>, <code>include</code>, or
     * <code>type</code> must be specified. </p>
     */
    private String include = null;

    /**
     * <p> Context-relative path of the input form to which control should be
     * returned if a validation error is encountered.  Required if "name" is
     * specified and the input bean returns validation errors. </p>
     */
    private String input = null;

    /**
     * <p> Fully qualified Java class name of the <code>MultipartRequestHandler</code>
     * implementation class used to process multi-part request data for this
     * Action. </p>
     */
    private String multipartClass = null;

    /**
     * <p> Name of the form bean, if any, associated with this Action. </p>
     */
    private String name = null;

    /**
     * <p> General purpose configuration parameter that can be used to pass
     * extra information to the Action instance selected by this Action.
     * Struts does not itself use this value in any way. </p>
     */
    private String parameter = null;

    /**
     * <p> Context-relative path of the submitted request, starting with a
     * slash ("/") character, and omitting any filename extension if extension
     * mapping is being used. </p>
     */
    private String path = null;

    /**
     * <p> Prefix used to match request parameter names to form bean property
     * names, if any. </p>
     */
    private String prefix = null;

    /**
     * <p> Comma-delimited list of security role names allowed to request this
     * Action. </p>
     */
    private String roles = null;

    /**
     * <p> The set of security role names used to authorize access to this
     * Action, as an array for faster access. </p>
     */
    private String[] roleNames = {};

    /**
     * <p> Identifier of the scope ("request" or "session") within which our
     * form bean is accessed, if any. </p>
     */
    private String scope = "session";

    /**
     * <p> Suffix used to match request parameter names to form bean property
     * names, if any. </p>
     */
    private String suffix = null;

    /**
     * <p> Fully qualified Java class name of the <code>Action</code> class to
     * be used to process requests for this mapping if the
     * <code>forward</code> and <code>include</code> properties are not set.
     * Exactly one of <code>forward</code>, <code>include</code>, or
     * <code>type</code> must be specified.
     */
    private String type = null;

    /**
     * <p> Indicates Action be configured as the default one for this module,
     * when true.
     */
    private boolean unknown = false;

    /**
     * Indicates whether the "validate" property has been set or not.
     */
    private boolean validateSet = false;

    /**
     * <p> Should the <code>validate()</code> method of the form bean
     * associated with this action be called?
     */
    private boolean validate = true;

    /**
     * <p> The name of a <code>commons-chain</code> command which should be
     * executed as part of the processing of this action.
     *
     * @since Struts 1.3.0
     */
    private String command = null;

    /**
     * <p> The name of a <code>commons-chain</code> catalog in which
     * <code>command</code> should be sought.  If a <code>command</code> is
     * defined and this property is undefined, the "default" catalog will be
     * used. This is likely to be infrequently used after a future release of
     * <code>commons-chain</code> supports a one-string expression of a
     * catalog/chain combination.
     *
     * @since Struts 1.3.0
     */
    private String catalog = null;

    /**
     * <p>The internal name of this action mapping. If an action has a name, it may be used
     * as a shortcut in a URI. For example, an action with an identification of "editPerson"
     * may be internally forwarded as "editPerson?id=1" which will then resolve to the
     * real URI path at execution time.</p>
     *
     * @return the actionId
     * @since Struts 1.3.6
     */
    public String getActionId() {
        return this.actionId;
    }

    /**
     * <p>The internal name of this action mapping. The name is not inheritable,
     * may not contain a forward slash, and must be unique within a module. </p>
     *
     * @param aid the action identifier
     * @throws IllegalStateException if the configuration is frozen
     * @throws IllegalArgumentException if the identifier contains a forward slash
     * @since Struts 1.3.6
     */
    public void setActionId(final String aid) {
        super.throwIfConfigured();

        if ((aid != null) && (aid.indexOf('/') > -1)) {
            throw new IllegalArgumentException(
                    "actionId '" + aid + "' may not contain a forward slash");
        }

        this.actionId = aid;
    }

    /**
     * <p> The module configuration with which we are associated.
     * @return ModuleConfig
     */
    public ModuleConfig getModuleConfig() {
        return this.moduleConfig;
    }

    /**
     * <p> The module configuration with which we are associated.
     * @param config ModuleConfig
     */
    public void setModuleConfig(final ModuleConfig config) {
        super.throwIfConfigured();

        this.moduleConfig = config;
    }

    /**
     * <p> Returns the request-scope or session-scope attribute name under
     * which our form bean is accessed, if it is different from the form
     * bean's specified <code>name</code>.
     *
     * @return attribute name under which our form bean is accessed.
     */
    public String getAttribute() {
        if (this.attribute == null) {
            return this.name;
        }
        return this.attribute;
    }

    /**
     * <p> Set the request-scope or session-scope attribute name under which
     * our form bean is accessed, if it is different from the form bean's
     * specified <code>name</code>.
     *
     * @param val the request-scope or session-scope attribute name
     *                  under which our form bean is access.
     */
    public void setAttribute(final String val) {
        super.throwIfConfigured();

        this.attribute = val;
    }

    /**
     * <p>Accessor for cancellable property</p>
     *
     * @return True if Action can be cancelled
     */
    public boolean getCancellable() {
        return this.cancellable;
    }

    /**
     * <p>Mutator for for cancellable property</p>
     *
     * @param val boolean
     */
    public void setCancellable(final boolean val) {
        super.throwIfConfigured();

        this.cancellable = val;
        this.cancellableSet = true;
    }

    /**
     * <p>Returns the path of the ActionConfig that this object should inherit
     * properties from.</p>
     *
     * @return the path of the ActionConfig that this object should inherit
     *         properties from.
     */
    public String getExtends() {
        return this.inherit;
    }

    /**
     * <p>Set the path of the ActionConfig that this object should inherit
     * properties from.</p>
     *
     * @param val the path of the ActionConfig that this object should
     *                inherit properties from.
     */
    public void setExtends(final String val) {
        super.throwIfConfigured();

        this.inherit = val;
    }

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

    /**
     * <p> Returns context-relative path of the web application resource that
     * will process this request.
     *
     * @return context-relative path of the web application resource that will
     *         process this request.
     */
    public String getForward() {
        return this.forward;
    }

    /**
     * <p> Set the context-relative path of the web application resource that
     * will process this request. Exactly one of <code>forward</code>,
     * <code>include</code>, or <code>type</code> must be specified.
     *
     * @param val context-relative path of the web application resource
     *                that will process this request.
     */
    public void setForward(final String val) {
        super.throwIfConfigured();

        this.forward = val;
    }

    /**
     * <p> Context-relative path of the web application resource that will
     * process this request.
     *
     * @return Context-relative path of the web application resource that will
     *         process this request.
     */
    public String getInclude() {
        return this.include;
    }

    /**
     * <p> Set context-relative path of the web application resource that will
     * process this request. Exactly one of <code>forward</code>,
     * <code>include</code>, or <code>type</code> must be specified.
     *
     * @param val context-relative path of the web application resource
     *                that will process this request.
     */
    public void setInclude(final String val) {
        super.throwIfConfigured();

        this.include = val;
    }

    /**
     * <p> Get the context-relative path of the input form to which control
     * should be returned if a validation error is encountered.
     *
     * @return context-relative path of the input form to which control should
     *         be returned if a validation error is encountered.
     */
    public String getInput() {
        return this.input;
    }

    /**
     * <p> Set the context-relative path of the input form to which control
     * should be returned if a validation error is encountered.  Required if
     * "name" is specified and the input bean returns validation errors.
     *
     * @param val context-relative path of the input form to which control
     *              should be returned if a validation error is encountered.
     */
    public void setInput(final String val) {
        super.throwIfConfigured();

        this.input = val;
    }

    /**
     * <p> Return the fully qualified Java class name of the
     * <code>MultipartRequestHandler</code> implementation class used to
     * process multi-part request data for this Action.
     * @return MultipartClass
     */
    public String getMultipartClass() {
        return this.multipartClass;
    }

    /**
     * <p> Set the fully qualified Java class name of the
     * <code>MultipartRequestHandler</code> implementation class used to
     * process multi-part request data for this Action.
     *
     * @param multipart fully qualified class name of the
     *                       <code>MultipartRequestHandler</code>
     *                       implementation class.
     */
    public void setMultipartClass(final String multipart) {
        super.throwIfConfigured();

        this.multipartClass = multipart;
    }

    /**
     * <p> Return name of the form bean, if any, associated with this Action.
     * @return Name
     */
    public String getName() {
        return this.name;
    }

    /**
     * @param val name of the form bean associated with this Action.
     */
    public void setName(final String val) {
        super.throwIfConfigured();

        this.name = val;
    }

    /**
     * <p> Return general purpose configuration parameter that can be used to
     * pass extra information to the Action instance selected by this Action.
     * Struts does not itself use this value in any way.
     * @return Parameter
     */
    public String getParameter() {
        return this.parameter;
    }

    /**
     * <p> General purpose configuration parameter that can be used to pass
     * extra information to the Action instance selected by this Action.
     * Struts does not itself use this value in any way.
     *
     * @param val General purpose configuration parameter.
     */
    public void setParameter(final String val) {
        super.throwIfConfigured();

        this.parameter = val;
    }

    /**
     * <p> Return context-relative path of the submitted request, starting
     * with a slash ("/") character, and omitting any filename extension if
     * extension mapping is being used.
     * @return Path
     */
    public String getPath() {
        return this.path;
    }

    /**
     * <p> Set context-relative path of the submitted request, starting with a
     * slash ("/") character, and omitting any filename extension if extension
     * mapping is being used.
     *
     * @param val context-relative path of the submitted request.
     */
    public void setPath(final String val) {
        super.throwIfConfigured();

        this.path = val;
    }

    /**
     * <p> Retruns prefix used to match request parameter names to form bean
     * property names, if any.
     * @return Prefix
     */
    public String getPrefix() {
        return this.prefix;
    }

    /**
     * @param val Prefix used to match request parameter names to form bean
     *               property names, if any.
     */
    public void setPrefix(final String val) {
        super.throwIfConfigured();

        this.prefix = val;
    }

    /**
     * @return Roles
     */
    public String getRoles() {
        return this.roles;
    }

    /**
     * @param val String
     */
    public void setRoles(final String val) {
        super.throwIfConfigured();

        this.roles = val;

        if (val == null) {
            this.roleNames = new String[0];
            return;
        }

        ArrayList<String> list = new ArrayList<>();
        String rl = val;
        while (true) {
            int comma = rl.indexOf(',');

            if (comma < 0) {
                break;
            }

            list.add(rl.substring(0, comma).trim());
            rl = rl.substring(comma + 1);
        }

        rl = rl.trim();

        if (rl.length() > 0) {
            list.add(rl);
        }

        this.roleNames = list.toArray(new String[list.size()]);
    }

    /**
     * <p> Get array of security role names used to authorize access to this Action.
     * @return RoleNames
     */
    public String[] getRoleNames() {
        return this.roleNames != null ? this.roleNames.clone() : new String[0];
    }

    /**
     * <p> Get the scope ("request" or "session") within which our form bean
     * is accessed, if any.
     * @return Scope
     */
    public String getScope() {
        return this.scope;
    }

    /**
     * @param val scope ("request" or "session") within which our form bean
     *              is accessed, if any.
     */
    public void setScope(final String val) {
        super.throwIfConfigured();

        this.scope = val;
    }

    /**
     * <p> Return suffix used to match request parameter names to form bean
     * property names, if any. </p>
     * @return Suffix
     */
    public String getSuffix() {
        return this.suffix;
    }

    /**
     * @param val Suffix used to match request parameter names to form bean
     *               property names, if any.
     */
    public void setSuffix(final String val) {
        super.throwIfConfigured();

        this.suffix = val;
    }

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

    /**
     * @param val String
     */
    public void setType(final String val) {
        super.throwIfConfigured();

        this.type = val;
    }

    /**
     * <p> Determine whether Action is configured as the default one for this
     * module. </p>
     * @return boolean
     */
    public boolean getUnknown() {
        return this.unknown;
    }

    /**
     * @param val Indicates Action is configured as the default one for
     *                this module, when true.
     */
    public void setUnknown(final boolean val) {
        super.throwIfConfigured();

        this.unknown = val;
    }

    /**
     * @return boolean
     */
    public boolean getValidate() {
        return this.validate;
    }

    /**
     * @param val boolean
     */
    public void setValidate(final boolean val) {
        super.throwIfConfigured();

        this.validate = val;
        this.validateSet = true;
    }

    /**
     * <p> Get the name of a <code>commons-chain</code> command which should
     * be executed as part of the processing of this action. </p>
     *
     * @return name of a <code>commons-chain</code> command which should be
     *         executed as part of the processing of this action.
     * @since Struts 1.3.0
     */
    public String getCommand() {
        return this.command;
    }

    /**
     * <p> Get the name of a <code>commons-chain</code> catalog in which a
     * specified command should be sought.  This is likely to be infrequently
     * used after a future release of <code>commons-chain</code> supports a
     * one-string expression of a catalog/chain combination. </p>
     *
     * @return name of a <code>commons-chain</code> catalog in which a
     *         specified command should be sought.
     * @since Struts 1.3.0
     */
    public String getCatalog() {
        return this.catalog;
    }

    /**
     * <p> Set the name of a <code>commons-chain</code> command which should
     * be executed as part of the processing of this action. </p>
     *
     * @param val name of a <code>commons-chain</code> command which
     *                should be executed as part of the processing of this
     *                action.
     * @since Struts 1.3.0
     */
    public void setCommand(final String val) {
        super.throwIfConfigured();

        this.command = val;
    }

    /**
     * <p> Set the name of a <code>commons-chain</code> catalog in which a
     * specified command should be sought. This is likely to be infrequently
     * used after a future release of <code>commons-chain</code> supports a
     * one-string expression of a catalog/chain combination. </p>
     *
     * @param val name of a <code>commons-chain</code> catalog in which a
     *                specified command should be sought.
     * @since Struts 1.3.0
     */
    public void setCatalog(final String val) {
        super.throwIfConfigured();

        this.catalog = val;
    }

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

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

        while (ancestorPath != null) {
            // check if we have the same path as an ancestor
            if (getPath().equals(ancestorPath)) {
                return true;
            }

            // get our ancestor's ancestor
            ActionConfig ancestor = config.get(ancestorPath);
            if (ancestor != null) {
                ancestorPath = ancestor.getExtends();
            } else {
                ancestorPath = null;
            }
        }

        return false;
    }

    /**
     * <p>Compare the exception handlers of this action with that of the given
     * and copy those that are not present.</p>
     * @param baseConfig The action config to copy handlers from.
     * @see #inheritFrom(ActionConfig)
     */
    protected void inheritExceptionHandlers(final ActionConfig baseConfig) {

        super.throwIfConfigured();

        // Inherit exception handler configs
        ExceptionConfig[] baseHandlers = baseConfig.findExceptionConfigs();

        for (int i = 0; i < baseHandlers.length; i++) {
            ExceptionConfig baseHandler = baseHandlers[i];

            // Do we have this handler?
            ExceptionConfig copy = findExceptionConfig(baseHandler.getType());

            if (copy == null) {
                try {
                    copy = ResponseUtils.applicationInstance(baseHandler.getClass().getName());
                    BeanUtils.copyProperties(copy, baseHandler);
                } catch (final ReflectiveOperationException e) {
                    throw new RuntimeException(e);
                }
                addExceptionConfig(copy);
                copy.setProperties(baseHandler.copyProperties());
            } else {
                // process any extension that this config might have
                copy.processExtends(this.moduleConfig.getExceptionConfigs(), this.exceptions);
            }
        }
    }

    /**
     * <p>Compare the forwards of this action with that of the given and copy
     * those that are not present.</p>
     * @param baseConfig The action config to copy forwards from.
     * @see #inheritFrom(ActionConfig)
     */
    protected void inheritForwards(final ActionConfig baseConfig) {

        super.throwIfConfigured();

        // Inherit forward configs
        ForwardConfig[] baseForwards = baseConfig.findForwardConfigs();

        for (int i = 0; i < baseForwards.length; i++) {
            ForwardConfig baseForward = baseForwards[i];

            // Do we have this forward?
            ForwardConfig copy = findForwardConfig(baseForward.getName());

            if (copy == null) {
                try {
                    copy = ResponseUtils.applicationInstance(baseForward.getClass().getName());
                    BeanUtils.copyProperties(copy, baseForward);
                } catch (final ReflectiveOperationException e) {
                    throw new RuntimeException(e);
                }
                addForwardConfig(copy);
                copy.setProperties(baseForward.copyProperties());
            } else {
                // process any extension for this forward
                copy.processExtends(getModuleConfig().getForwardConfigs(), this.forwards);
            }
        }
    }

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

    /**
     * <p> Add a new <code>ExceptionConfig</code> instance to the set
     * associated with this action. </p>
     *
     * @param config The new configuration instance to be added
     * @throws IllegalStateException if this module configuration has been
     *                               frozen
     */
    public void addExceptionConfig(final ExceptionConfig config) {
        super.throwIfConfigured();

        this.exceptions.put(config.getType(), config);
    }

    /**
     * <p> Add a new <code>ForwardConfig</code> instance to the set of global
     * forwards associated with this action. </p>
     *
     * @param config The new configuration instance to be added
     * @throws IllegalStateException if this module configuration has been
     *                               frozen
     */
    public void addForwardConfig(final ForwardConfig config) {
        super.throwIfConfigured();

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

    /**
     * <p> Return the exception configuration for the specified type, if any;
     * otherwise return <code>null</code>. </p>
     * @param val Exception class name to find a configuration for
     * @return ExceptionConfig
     */
    public ExceptionConfig findExceptionConfig(final String val) {
        return this.exceptions.get(val);
    }

    /**
     * <p> Return the exception configurations for this action.  If there are
     * none, a zero-length array is returned. </p>
     * @return ExceptionConfig
     */
    public ExceptionConfig[] findExceptionConfigs() {
        ExceptionConfig[] results = new ExceptionConfig[this.exceptions.size()];

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

    /**
     * <p>Find and return the <code>ExceptionConfig</code> instance defining
     * how <code>Exceptions</code> of the specified type should be handled.
     * This is performed by checking local and then global configurations for
     * the specified exception's class, and then looking up the superclass
     * chain (again checking local and then global configurations). If no
     * handler configuration can be found, return <code>null</code>.</p>
     *
     * <p>Introduced in <code>ActionMapping</code> in Struts 1.1, but pushed
     * up to <code>ActionConfig</code> in Struts 1.2.0.</p>
     * @param val Exception class for which to find a handler
     * @return ExceptionConfig
     * @since Struts 1.2.0
     */
    public ExceptionConfig findException(final Class<?> val) {
        // Check through the entire superclass hierarchy as needed
        ExceptionConfig config;
        Class<?> cls = val;
        while (true) {
            // Check for a locally defined handler
            String clsName = cls.getName();

            LOG.debug("findException: look locally for " + clsName);
            config = findExceptionConfig(clsName);

            if (config != null) {
                return config;
            }

            // Check for a globally defined handler
            LOG.debug("findException: look globally for " + clsName);
            config = getModuleConfig().findExceptionConfig(clsName);

            if (config != null) {
                return config;
            }

            // Loop again for our superclass (if any)
            cls = cls.getSuperclass();

            if (cls == null) {
                break;
            }
        }

        // No handler has been configured
        return null;
    }

    /**
     * <p> Return the forward configuration for the specified key, if any;
     * otherwise return <code>null</code>. </p>
     * @param val Name of the forward configuration to return
     * @return ForwardConfig
     */
    public ForwardConfig findForwardConfig(final String val) {
        return this.forwards.get(val);
    }

    /**
     * <p> Return all forward configurations for this module.  If there are
     * none, a zero-length array is returned. </p>
     * @return ForwardConfig
     */
    public ForwardConfig[] findForwardConfigs() {
        ForwardConfig[] results = new ForwardConfig[this.forwards.size()];

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

    /**
     * <p> Freeze the configuration of this action. </p>
     */
    @Override
    public void freeze() {
        super.freeze();

        ExceptionConfig[] econfigs = findExceptionConfigs();

        for (int i = 0; i < econfigs.length; i++) {
            econfigs[i].freeze();
        }

        ForwardConfig[] fconfigs = findForwardConfigs();

        for (int i = 0; i < fconfigs.length; i++) {
            fconfigs[i].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 ActionConfig config) {

        super.throwIfConfigured();

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

        if (!this.cancellableSet) {
            setCancellable(config.getCancellable());
        }

        if (getCatalog() == null) {
            setCatalog(config.getCatalog());
        }

        if (getCommand() == null) {
            setCommand(config.getCommand());
        }

        if (getForward() == null) {
            setForward(config.getForward());
        }

        if (getInclude() == null) {
            setInclude(config.getInclude());
        }

        if (getInput() == null) {
            setInput(config.getInput());
        }

        if (getMultipartClass() == null) {
            setMultipartClass(config.getMultipartClass());
        }

        if (getName() == null) {
            setName(config.getName());
        }

        if (getParameter() == null) {
            setParameter(config.getParameter());
        }

        if (getPath() == null) {
            setPath(config.getPath());
        }

        if (getPrefix() == null) {
            setPrefix(config.getPrefix());
        }

        if (getRoles() == null) {
            setRoles(config.getRoles());
        }

        if (getScope().equals("session")) {
            setScope(config.getScope());
        }

        if (getSuffix() == null) {
            setSuffix(config.getSuffix());
        }

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

        if (!getUnknown()) {
            setUnknown(config.getUnknown());
        }

        if (!this.validateSet) {
            setValidate(config.getValidate());
        }

        inheritExceptionHandlers(config);
        inheritForwards(config);
        inheritProperties(config);
    }

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

        super.throwIfConfigured();

        String ancestorPath = getExtends();

        if (!this.extensionProcessed && (ancestorPath != null)) {
            ActionConfig baseConfig =
                config.get(ancestorPath);

            if (baseConfig == null) {
                throw new NullPointerException("Unable to find "
                    + "action for '" + ancestorPath + "' to extend.");
            }

            // Check against circular inheritance and make sure the base
            //  config's own extends has been processed already
            if (checkCircularInheritance(config)) {
                throw new IllegalArgumentException(
                    "Circular inheritance detected for action " + getPath());
            }

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

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

        this.extensionProcessed = true;
    }

    /**
     * <p> Remove the specified exception configuration instance. </p>
     *
     * @param config ExceptionConfig instance to be removed
     * @throws IllegalStateException if this module configuration has been
     *                               frozen
     */
    public void removeExceptionConfig(final ExceptionConfig config) {
        super.throwIfConfigured();

        this.exceptions.remove(config.getType());
    }

    /**
     * <p> Remove the specified forward configuration instance. </p>
     *
     * @param config ForwardConfig instance to be removed
     * @throws IllegalStateException if this module configuration has been
     *                               frozen
     */
    public void removeForwardConfig(final ForwardConfig config) {
        super.throwIfConfigured();

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

    Map<String, ForwardConfig> getForwards() {
        return this.forwards;
    }

    /**
     * <p> Return a String representation of this object. </p>
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("ActionConfig[");

        sb.append("cancellable=").append(this.cancellable);

        sb.append(",path=").append(this.path);

        sb.append(",validate=").append(this.validate);

        if (this.actionId != null) {
            sb.append(",actionId=").append(this.actionId);
        }

        if (this.attribute != null) {
            sb.append(",attribute=").append(this.attribute);
        }

        if (this.catalog != null) {
            sb.append(",catalog=").append(this.catalog);
        }

        if (this.command != null) {
            sb.append(",command=").append(this.command);
        }

        if (this.inherit != null) {
            sb.append(",extends=").append(this.inherit);
        }

        if (this.forward != null) {
            sb.append(",forward=").append(this.forward);
        }

        if (this.include != null) {
            sb.append(",include=").append(this.include);
        }

        if (this.input != null) {
            sb.append(",input=").append(this.input);
        }

        if (this.multipartClass != null) {
            sb.append(",multipartClass=").append(this.multipartClass);
        }

        if (this.name != null) {
            sb.append(",name=").append(this.name);
        }

        if (this.parameter != null) {
            sb.append(",parameter=").append(this.parameter);
        }

        if (this.prefix != null) {
            sb.append(",prefix=").append(this.prefix);
        }

        if (this.roles != null) {
            sb.append(",roles=").append(this.roles);
        }

        if (this.scope != null) {
            sb.append(",scope=").append(this.scope);
        }

        if (this.suffix != null) {
            sb.append(",suffix=").append(this.suffix);
        }

        if (this.type != null) {
            sb.append(",type=").append(this.type);
        }

        return sb.toString();
    }

    /**
     * @param mc ModuleConfig
     * @param ac ActionConfig
     * @param ec ExceptionConfig
     */
    public static void extendsExceptionConfig(final ModuleConfig mc,
            final ActionConfig ac, final ExceptionConfig ec) {

        ec.processExtends(mc.getExceptionConfigs(), ac != null ? ac.exceptions : null);
    }

    /**
     * @param mc ModuleConfig
     * @param ac ActionConfig
     * @param fc ForwardConfig
     */
    public static void extendsForwardConfig(final ModuleConfig mc,
            final ActionConfig ac, final ForwardConfig fc) {

        fc.processExtends(mc.getForwardConfigs(), ac != null ? ac.forwards : null);
    }
}
