/*
 * $Id: TestActionServlet.java 471754 2006-11-06 14:55:09Z husted $
 *
 * 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.action;

import java.net.URL;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.UnavailableException;

import org.apache.struts.config.ActionConfig;
import org.apache.struts.config.ExceptionConfig;
import org.apache.struts.config.FormBeanConfig;
import org.apache.struts.config.FormPropertyConfig;
import org.apache.struts.config.ForwardConfig;
import org.apache.struts.config.ModuleConfig;
import org.apache.struts.config.ModuleConfigFactory;
import org.junit.Test;

import junit.framework.TestCase;

/**
 * Suite of unit tests for the <code>org.apache.struts.action.ActionServlet</code>
 * class.
 */
public class TestActionServlet extends TestCase {

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

    /**
     * The ModuleConfig we'll use.
     */
    private ModuleConfig moduleConfig = null;

    /**
     * The common form bean we'll use.
     */
    private FormBeanConfig baseFormBean = null;

    /**
     * The common exception config we'll use.
     */
    private ExceptionConfig baseException = null;

    /**
     * The common action config we'll use.
     */
    private ActionMapping baseAction = null;

    /**
     * The common action forward we'll use.
     */
    private ActionForward baseForward = null;

    /**
     * The ActionServlet we'll test.
     */
    private ActionServlet actionServlet = null;

    // ------------------------------------------ Constructors, suite, and main

    /**
     * Defines the testcase name for JUnit.
     *
     * @param theName the testcase's name.
     */
    public TestActionServlet(final String theName) {
        super(theName);
    }

    // ------------------------------------------------- setUp() and tearDown()

    /**
     * Set up instance variables required by this test case.
     */
    @Override
    public void setUp() throws Exception {
        this.actionServlet = new ActionServlet();

        ModuleConfigFactory factoryObject = ModuleConfigFactory.createFactory();

        this.moduleConfig = factoryObject.createModuleConfig("");

        // Setup the base form
        this.baseFormBean = new FormBeanConfig();
        this.baseFormBean.setName("baseForm");
        this.baseFormBean.setType("org.apache.struts.mock.MockActionForm");

        // Set up id, name, and score
        FormPropertyConfig property = new FormPropertyConfig();

        property.setName("id");
        property.setType("java.lang.String");
        this.baseFormBean.addFormPropertyConfig(property);

        property = new FormPropertyConfig();
        property.setName("name");
        property.setType("java.lang.String");
        this.baseFormBean.addFormPropertyConfig(property);

        property = new FormPropertyConfig();
        property.setName("score");
        property.setType("java.lang.String");
        this.baseFormBean.addFormPropertyConfig(property);

        // Setup the exception handler
        this.baseException = new ExceptionConfig();
        this.baseException.setType("java.lang.NullPointerException");
        this.baseException.setKey("msg.exception.npe");

        // Setup the forward config
        this.baseForward = new ActionForward("success", "/succes.jsp", false);

        setUpAction();
    }

    void setUpAction() {

        // Setup the action config
        this.baseAction = new ActionMapping();
        this.baseAction.setPath("/index");
        this.baseAction.setType("org.apache.struts.actions.DummyAction");
        this.baseAction.setName("someForm");
        this.baseAction.setInput("/input.jsp");
        this.baseAction.addForwardConfig(new ActionForward("next", "/next.jsp", false));
        this.baseAction.addForwardConfig(new ActionForward("prev", "/prev.jsp", false));

        ExceptionConfig exceptionConfig = new ExceptionConfig();

        exceptionConfig.setType("java.sql.SQLException");
        exceptionConfig.setKey("msg.exception.sql");
        this.baseAction.addExceptionConfig(exceptionConfig);

        // Nothing is registered to our module config until they are needed
    }

    /**
     * Tear down instance variables required by this test case.
     */
    @Override
    public void tearDown() {
        this.moduleConfig = null;
    }

    // ----------------------------- initInternal() and destroyInternal() tests

    /**
     * Verify that we can initialize and destroy our internal message
     * resources object.
     */
    @Test
    public void testInitDestroyInternal() {
        ActionServlet servlet = new ActionServlet();

        assertTrue("internal was initialized", servlet.getInternal() != null);
        assertNotNull("internal of correct type",
            servlet.getInternal());
    }

    /**
     * Test class loader resolution and splitting.
     * @throws Exception Exception
     */
    @Test
    public void notestSplitAndResolvePaths() throws Exception {
        ActionServlet servlet = new ActionServlet();
        List<URL> list = servlet.splitAndResolvePaths(
                "org/apache/struts/config/struts-config.xml");

        assertNotNull(list);
        assertTrue("List size should be 1", list.size() == 1);

        list = servlet.splitAndResolvePaths(
                "org/apache/struts/config/struts-config.xml, "
                + "org/apache/struts/config/struts-config-1.1.xml");
        assertNotNull(list);
        assertTrue("List size should be 2, was " + list.size(), list.size() == 2);

        list = servlet.splitAndResolvePaths("META-INF/MANIFEST.MF");
        assertNotNull(list);
        assertTrue("Number of manifests should be more than 5, was "
            + list.size(), list.size() > 5);

        // test invalid path
        try {
            list = servlet.splitAndResolvePaths(
                    "org/apache/struts/config/struts-asdfasdfconfig.xml");
            assertNotNull(list);
            fail("Should have thrown an exception on bad path");
        } catch (final NullPointerException ex) {
            // correct behavior since internal error resources aren't loaded
            ex.printStackTrace();
        }
    }

    // --------------------------------------------------- FormBeanConfig Tests

    /**
     * Test that nothing fails if there are no extensions.
     */
    @Test
    public void testInitModuleFormBeansNoExtends() {
        this.moduleConfig.addFormBeanConfig(this.baseFormBean);

        try {
            this.actionServlet.initModuleExceptionConfigs(this.moduleConfig);
        } catch (final ServletException e) {
            fail("Unexpected exception caught." + e);
        }
    }

    /**
     * Test that initModuleFormBeans throws an exception when a form with a
     * null type is present.
     */
    @Test
    public void testInitModuleFormBeansNullFormType() {
        FormBeanConfig formBean = new FormBeanConfig();

        formBean.setName("noTypeForm");
        this.moduleConfig.addFormBeanConfig(formBean);

        try {
            this.actionServlet.initModuleFormBeans(this.moduleConfig);
            fail("An exception should've been thrown here.");
        } catch (final UnavailableException e) {
            // success
            e.printStackTrace();
        } catch (final ServletException e) {
            fail("Unrecognized exception thrown: " + e);
        }
    }

    /**
     * Test that initModuleFormBeans throws an exception when a form whose
     * prop type is null is present.
     */
    @Test
    public void testInitModuleFormBeansNullPropType() {
        this.moduleConfig.addFormBeanConfig(this.baseFormBean);
        this.baseFormBean.findFormPropertyConfig("name").setType(null);

        try {
            this.actionServlet.initModuleFormBeans(this.moduleConfig);
            fail("An exception should've been thrown here.");
        } catch (final UnavailableException e) {
            // success
            e.printStackTrace();
        } catch (final ServletException e) {
            fail("Unrecognized exception thrown: " + e);
        }
    }

    /**
     * Test that processFormBeanExtension() calls processExtends()
     * @throws ServletException ServletException
     */
    @Test
    public void testProcessFormBeanExtension()
            throws ServletException {
        CustomFormBeanConfig form = new CustomFormBeanConfig();

        this.actionServlet.processFormBeanExtension(form, this.moduleConfig);

        assertTrue("processExtends() was not called", form.processExtendsCalled());
    }

    /**
     * Make sure processFormBeanConfigClass() returns an instance of the
     * correct class if the base config is using a custom class.
     * @throws Exception Exception
     */
    @Test
    public void testProcessFormBeanConfigClass()
            throws Exception {
        CustomFormBeanConfig customBase = new CustomFormBeanConfig();

        customBase.setName("customBase");
        this.moduleConfig.addFormBeanConfig(customBase);

        FormBeanConfig customSub = new FormBeanConfig();

        customSub.setName("customSub");
        customSub.setExtends("customBase");
        customSub.setType("org.apache.struts.action.DynaActionForm");
        this.moduleConfig.addFormBeanConfig(customSub);

        FormBeanConfig result = this.actionServlet.processFormBeanConfigClass(
                customSub, this.moduleConfig);

        assertTrue("Incorrect class of form bean config",
            result instanceof CustomFormBeanConfig);
        assertEquals("Incorrect name", customSub.getName(), result.getName());
        assertEquals("Incorrect type", customSub.getType(), result.getType());
        assertEquals("Incorrect extends", customSub.getExtends(),
            result.getExtends());
        assertEquals("Incorrect 'restricted' value", customSub.isRestricted(),
            result.isRestricted());

        assertSame("Result was not registered in the module config", result,
                this.moduleConfig.findFormBeanConfig("customSub"));
    }

    /**
     * Make sure processFormBeanConfigClass() returns what it was given if the
     * form passed to it doesn't extend anything.
     * @throws Exception Exception
     */
    public void testProcessFormBeanConfigClassNoExtends()
            throws Exception {
        this.moduleConfig.addFormBeanConfig(this.baseFormBean);

        FormBeanConfig result = null;

        try {
            result = this.actionServlet.processFormBeanConfigClass(
                    this.baseFormBean, this.moduleConfig);
        } catch (final UnavailableException e) {
            fail("An exception should not be thrown when there's nothing to do" + e);
        }

        assertSame("Result should be the same as the input.", this.baseFormBean, result);
    }

    /**
     * Make sure processFormBeanConfigClass() returns the same class instance
     * if the base config isn't using a custom class.
     * @throws Exception Exception
     */
    public void testProcessFormBeanConfigClassSubFormCustomClass()
            throws Exception {
        this.moduleConfig.addFormBeanConfig(this.baseFormBean);

        FormBeanConfig customSub = new FormBeanConfig();

        customSub.setName("customSub");
        customSub.setExtends("baseForm");
        this.moduleConfig.addFormBeanConfig(customSub);

        FormBeanConfig result = this.actionServlet.processFormBeanConfigClass(
                customSub, this.moduleConfig);

        assertSame("The instance returned should be the param given it.",
            customSub, result);
    }

    /**
     * Make sure the code throws the correct exception when it can't create an
     * instance of the base config's custom class.
     */
    public void notestProcessFormBeanConfigClassError() {
        CustomFormBeanConfigArg customBase =
            new CustomFormBeanConfigArg("customBase");

        this.moduleConfig.addFormBeanConfig(customBase);

        FormBeanConfig customSub = new FormBeanConfig();

        customSub.setName("customSub");
        customSub.setExtends("customBase");
        this.moduleConfig.addFormBeanConfig(customSub);

        try {
            this.actionServlet.processFormBeanConfigClass(customSub, this.moduleConfig);
            fail("Exception should be thrown");
        } catch (final UnavailableException e) {
            // success
            e.printStackTrace();
        } catch (final ServletException e) {
            fail("Unexpected exception thrown." + e);
        }
    }

    /**
     * Test the case where the subform has already specified its own form bean
     * config class.  If the code still attempts to create a new instance, an
     * error will be thrown.
     */
    public void testProcessFormBeanConfigClassOverriddenSubFormClass() {
        CustomFormBeanConfigArg customBase =
            new CustomFormBeanConfigArg("customBase");

        this.moduleConfig.addFormBeanConfig(customBase);

        FormBeanConfig customSub = new CustomFormBeanConfigArg("customSub");

        customSub.setExtends("customBase");
        this.moduleConfig.addFormBeanConfig(customSub);

        try {
            this.actionServlet.processFormBeanConfigClass(customSub, this.moduleConfig);
        } catch (final ServletException e) {
            fail("Exception should not be thrown" + e);
        }
    }

    // -------------------------------------------------- ExceptionConfig Tests

    /**
     * Test that nothing fails if there are no extensions.
     */
    public void testInitModuleExceptionConfigsNoExtends() {
        this.moduleConfig.addExceptionConfig(this.baseException);

        try {
            this.actionServlet.initModuleExceptionConfigs(this.moduleConfig);
        } catch (final ServletException e) {
            fail("Unexpected exception caught." + e);
        }
    }

    /**
     * Test that initModuleExceptionConfigs throws an exception when a handler
     * with a null key is present.
     */
    public void testInitModuleExceptionConfigsNullFormType() {
        ExceptionConfig handler = new ExceptionConfig();

        handler.setType("java.lang.NullPointerException");
        this.moduleConfig.addExceptionConfig(handler);

        try {
            this.actionServlet.initModuleExceptionConfigs(this.moduleConfig);
            fail("An exception should've been thrown here.");
        } catch (final UnavailableException e) {
            // success
            e.printStackTrace();
        } catch (final ServletException e) {
            fail("Unrecognized exception thrown: " + e);
        }
    }

    /**
     * Test that processExceptionExtension() calls processExtends()
     * @throws ServletException ServletException
     */
    public void testProcessExceptionExtension()
            throws ServletException {
        CustomExceptionConfig handler = new CustomExceptionConfig();

        handler.setType("java.lang.NullPointerException");
        this.moduleConfig.addExceptionConfig(handler);
        this.actionServlet.processExceptionExtension(handler, this.moduleConfig, null);

        assertTrue("processExtends() was not called", handler.isProcessExtendsCalled());
    }

    /**
     * Make sure processExceptionConfigClass() returns an instance of the
     * correct class if the base config is using a custom class.
     * @throws Exception Exception
     */
    public void testProcessExceptionConfigClass()
            throws Exception {
        CustomExceptionConfig customBase = new CustomExceptionConfig();

        customBase.setType("java.lang.NullPointerException");
        customBase.setKey("msg.exception.npe");
        this.moduleConfig.addExceptionConfig(customBase);

        ExceptionConfig customSub = new ExceptionConfig();

        customSub.setType("java.lang.IllegalStateException");
        customSub.setExtends("java.lang.NullPointerException");
        this.moduleConfig.addExceptionConfig(customSub);

        ExceptionConfig result = this.actionServlet.processExceptionConfigClass(
                customSub, this.moduleConfig, null);

        assertTrue("Incorrect class of exception config",
            result instanceof CustomExceptionConfig);
        assertEquals("Incorrect type", customSub.getType(), result.getType());
        assertEquals("Incorrect key", customSub.getKey(), result.getKey());
        assertEquals("Incorrect extends", customSub.getExtends(), result.getExtends());

        assertSame("Result was not registered in the module config", result,
            this.moduleConfig.findExceptionConfig("java.lang.IllegalStateException"));
    }

    /**
     * Make sure processExceptionConfigClass() returns what it was given if
     * the handler passed to it doesn't extend anything.
     * @throws Exception Exception
     */
    public void testProcessExceptionConfigClassNoExtends()
            throws Exception {
        this.moduleConfig.addExceptionConfig(this.baseException);

        ExceptionConfig result = null;

        try {
            result = this.actionServlet.processExceptionConfigClass(
                    this.baseException, this.moduleConfig, null);
        } catch (final UnavailableException e) {
            fail("An exception should not be thrown when there's nothing to do" + e);
        }

        assertSame("Result should be the same as the input.", this.baseException, result);
    }

    /**
     * Make sure processExceptionConfigClass() returns the same class instance
     * if the base config isn't using a custom class.
     * @throws Exception Exception
     */
    public void testProcessExceptionConfigClassSubConfigCustomClass()
            throws Exception {
        this.moduleConfig.addExceptionConfig(this.baseException);

        ExceptionConfig customSub = new ExceptionConfig();

        customSub.setType("java.lang.IllegalStateException");
        customSub.setExtends("java.lang.NullPointerException");
        this.moduleConfig.addExceptionConfig(customSub);

        ExceptionConfig result = this.actionServlet.processExceptionConfigClass(
                customSub, this.moduleConfig, null);

        assertSame("The instance returned should be the param given it.",
            customSub, result);
    }

    /**
     * Make sure the code throws the correct exception when it can't create an
     * instance of the base config's custom class.
     */
    public void notestProcessExceptionConfigClassError() {
        ExceptionConfig customBase =
            new CustomExceptionConfigArg("java.lang.NullPointerException");

        this.moduleConfig.addExceptionConfig(customBase);

        ExceptionConfig customSub = new ExceptionConfig();

        customSub.setType("java.lang.IllegalStateException");
        customSub.setExtends("java.lang.NullPointerException");
        this.moduleConfig.addExceptionConfig(customSub);

        try {
            this.actionServlet.processExceptionConfigClass(customSub, this.moduleConfig, null);
            fail("Exception should be thrown");
        } catch (final UnavailableException e) {
            // success
            e.printStackTrace();
        } catch (final ServletException e) {
            fail("Unexpected exception thrown." + e);
        }
    }

    /**
     * Test the case where the subconfig has already specified its own config
     * class.  If the code still attempts to create a new instance, an error
     * will be thrown.
     */
    @Test
    public void testProcessExceptionConfigClassOverriddenSubFormClass() {
        this.moduleConfig.addExceptionConfig(this.baseException);

        ExceptionConfig customSub =
            new CustomExceptionConfigArg("java.lang.IllegalStateException");

        customSub.setExtends("java.lang.NullPointerException");
        this.moduleConfig.addExceptionConfig(customSub);

        try {
            this.actionServlet.processExceptionConfigClass(customSub, this.moduleConfig, null);
        } catch (final ServletException e) {
            fail("Exception should not be thrown" + e);
        }
    }

    // ---------------------------------------------------- ForwardConfig Tests

    /**
     * Test that nothing fails if there are no extensions.
     */
    @Test
    public void testInitModuleForwardConfigsNoExtends() {
        this.moduleConfig.addForwardConfig(this.baseForward);

        try {
            this.actionServlet.initModuleForwards(this.moduleConfig);
        } catch (final ServletException e) {
            fail("Unexpected exception caught." + e);
        }
    }

    /**
     * Test that initModuleForwards throws an exception when a forward with a
     * null path is present.
     */
    @Test
    public void testInitModuleForwardsNullFormType() {
        ActionForward forward = new ActionForward("success", null, false);

        this.moduleConfig.addForwardConfig(forward);

        try {
            this.actionServlet.initModuleForwards(this.moduleConfig);
            fail("An exception should've been thrown here.");
        } catch (final UnavailableException e) {
            // success
            e.printStackTrace();
        } catch (final ServletException e) {
            fail("Unrecognized exception thrown: " + e);
        }
    }

    /**
     * Test that processForwardExtension() calls processExtends()
     * @throws ServletException ServletException
     */
    @Test
    public void testProcessForwardExtension()
            throws ServletException {
        CustomForwardConfig forward = new CustomForwardConfig("forward", "/forward.jsp");

        this.moduleConfig.addForwardConfig(forward);
        this.actionServlet.processForwardExtension(forward, this.moduleConfig, null);

        assertTrue("processExtends() was not called", forward.isProcessExtendsCalled());
    }

    /**
     * Make sure processForwardConfigClass() returns an instance of the
     * correct class if the base config is using a custom class.
     * @throws Exception Exception
     */
    @Test
    public void testProcessForwardConfigClass()
            throws Exception {
        CustomForwardConfig customBase = new CustomForwardConfig("success", "/success.jsp");

        this.moduleConfig.addForwardConfig(customBase);

        ActionForward customSub = new ActionForward();

        customSub.setName("failure");
        customSub.setExtends("success");
        this.moduleConfig.addForwardConfig(customSub);

        ForwardConfig result = this.actionServlet.processForwardConfigClass(
                customSub, this.moduleConfig, null);

        assertTrue("Incorrect class of forward config",
            result instanceof CustomForwardConfig);
        assertEquals("Incorrect name", customSub.getName(), result.getName());
        assertEquals("Incorrect path", customSub.getPath(), result.getPath());
        assertEquals("Incorrect extends", customSub.getExtends(), result.getExtends());

        assertSame("Result was not registered in the module config", result,
                this.moduleConfig.findForwardConfig("failure"));
    }

    /**
     * Make sure processForwardConfigClass() returns what it was given if the
     * forward passed to it doesn't extend anything.
     * @throws Exception Exception
     */
    @Test
    public void testProcessForwardConfigClassNoExtends()
            throws Exception {
        this.moduleConfig.addForwardConfig(this.baseForward);

        ForwardConfig result = null;
        try {
            result = this.actionServlet.processForwardConfigClass(
                    this.baseForward, this.moduleConfig, null);
        } catch (final UnavailableException e) {
            fail("An exception should not be thrown when there's nothing to do" + e);
        }

        assertSame("Result should be the same as the input.", this.baseForward, result);
    }

    /**
     * Make sure processForwardConfigClass() returns the same class instance
     * if the base config isn't using a custom class.
     * @throws Exception Exception
     */
    @Test
    public void testProcessForwardConfigClassSubConfigCustomClass()
            throws Exception {
        this.moduleConfig.addForwardConfig(this.baseForward);

        ForwardConfig customSub = new ActionForward();
        customSub.setName("failure");
        customSub.setExtends("success");
        this.moduleConfig.addForwardConfig(customSub);

        ForwardConfig result = this.actionServlet.processForwardConfigClass(
                customSub, this.moduleConfig, null);

        assertSame("The instance returned should be the param given it.", customSub, result);
    }

    /**
     * Make sure the code throws the correct exception when it can't create an
     * instance of the base config's custom class.
     */
    @Test
    public void notestProcessForwardConfigClassError() {
        ForwardConfig customBase = new CustomForwardConfigArg("success", "/success.jsp");

        this.moduleConfig.addForwardConfig(customBase);

        ForwardConfig customSub = new ActionForward();

        customSub.setName("failure");
        customSub.setExtends("success");
        this.moduleConfig.addForwardConfig(customSub);

        try {
            this.actionServlet.processForwardConfigClass(customSub, this.moduleConfig, null);
            fail("Exception should be thrown");
        } catch (final UnavailableException e) {
            // success
            e.printStackTrace();
        } catch (final ServletException e) {
            fail("Unexpected exception thrown." + e);
        }
    }

    /**
     * Test the case where the subconfig has already specified its own config
     * class.  If the code still attempts to create a new instance, an error
     * will be thrown.
     */
    @Test
    public void testProcessForwardConfigClassOverriddenSubConfigClass() {
        this.moduleConfig.addForwardConfig(this.baseForward);

        ForwardConfig customSub = new CustomForwardConfigArg("failure", "/failure.jsp");

        customSub.setExtends("success");
        this.moduleConfig.addForwardConfig(customSub);

        try {
            this.actionServlet.processForwardConfigClass(customSub, this.moduleConfig, null);
        } catch (final ServletException e) {
            fail("Exception should not be thrown" + e);
        }
    }

    // --------------------------------------------------- ActionConfig Tests

    /**
     * Test that nothing fails if there are no extensions.
     */
    @Test
    public void testInitModuleActionConfigsNoExtends() {
        this.moduleConfig.addActionConfig(this.baseAction);

        try {
            this.actionServlet.initModuleActions(this.moduleConfig);
        } catch (final ServletException e) {
            fail("Unexpected exception caught." + e);
        }
    }

    /**
     * Test that processActionConfigExtension() calls processExtends()
     * throws ServletException ServletException
     * @throws ServletException ServletException
     */
    @Test
    public void testProcessActionExtension() throws ServletException {
        CustomActionConfig action = new CustomActionConfig("/action");

        this.moduleConfig.addActionConfig(action);
        this.actionServlet.processActionConfigExtension(action, this.moduleConfig);

        assertTrue("processExtends() was not called", action.isProcessExtendsCalled());
    }

    /**
     * Test that an ActionConfig's ForwardConfig can inherit from a
     * global ForwardConfig.
     * @throws ServletException ServletException
     */
    @Test
    public void testProcessActionExtensionWithForwardConfig()
            throws ServletException {
        ForwardConfig forwardConfig = new ForwardConfig();
        forwardConfig.setName("sub");
        forwardConfig.setExtends("success");
        this.baseAction.addForwardConfig(forwardConfig);

        this.moduleConfig.addActionConfig(this.baseAction);
        this.moduleConfig.addForwardConfig(this.baseForward);
        this.actionServlet.processActionConfigExtension(this.baseAction, this.moduleConfig);

        forwardConfig = this.baseAction.findForwardConfig("sub");

        assertEquals("'sub' forward's inheritance was not processed.",
                this.baseForward.getPath(), forwardConfig.getPath());
    }

    /**
     * Test that an ActionConfig's ExceptionConfig can inherit from a
     * global ExceptionConfig.
     * @throws ServletException ServletException
     */
    @Test
    public void testProcessActionExtensionWithExceptionConfig()
            throws ServletException {
        ExceptionConfig exceptionConfig = new ExceptionConfig();
        exceptionConfig.setType("SomeException");
        exceptionConfig.setExtends("java.lang.NullPointerException");
        this.baseAction.addExceptionConfig(exceptionConfig);

        this.moduleConfig.addActionConfig(this.baseAction);
        this.moduleConfig.addExceptionConfig(this.baseException);
        this.actionServlet.processActionConfigExtension(this.baseAction, this.moduleConfig);

        exceptionConfig = this.baseAction.findExceptionConfig("SomeException");

        assertEquals("SomeException's inheritance was not processed.",
                this.baseException.getKey(), exceptionConfig.getKey());
    }

    /**
     * Make sure processActionConfigClass() returns an instance of the correct
     * class if the base config is using a custom class.
     * @throws Exception Exception
     */
    @Test
    public void testProcessActionConfigClass() throws Exception {
        CustomActionConfig customBase = new CustomActionConfig("/base");

        this.moduleConfig.addActionConfig(customBase);

        ActionMapping customSub = new ActionMapping();

        customSub.setPath("/sub");
        customSub.setExtends("/base");
        this.moduleConfig.addActionConfig(customSub);

        ActionConfig result =
            this.actionServlet.processActionConfigClass(customSub, this.moduleConfig);

        assertTrue("Incorrect class of action config",
            result instanceof CustomActionConfig);
        assertEquals("Incorrect path", customSub.getPath(), result.getPath());
        assertEquals("Incorrect extends", customSub.getExtends(),
            result.getExtends());

        assertSame("Result was not registered in the module config", result,
            this.moduleConfig.findActionConfig("/sub"));
    }

    /**
     * Make sure processActionConfigClass() returns what it was given if the
     * action passed to it doesn't extend anything.
     */
    @Test
    public void testProcessActionConfigClassNoExtends() {
        this.moduleConfig.addActionConfig(this.baseAction);

        ActionConfig result = null;

        try {
            result =
                this.actionServlet.processActionConfigClass(this.baseAction, this.moduleConfig);
        } catch (final ServletException e) {
            fail("An exception should not be thrown here" + e);
        }

        assertSame("Result should be the same as the input.", this.baseAction, result);
    }

    /**
     * Make sure processActionConfigClass() returns the same class instance if
     * the base config isn't using a custom class.
     */
    @Test
    public void testProcessActionConfigClassSubConfigCustomClass() {
        this.moduleConfig.addActionConfig(this.baseAction);

        ActionConfig customSub = new ActionMapping();

        customSub.setPath("/sub");
        customSub.setExtends("/index");
        this.moduleConfig.addActionConfig(customSub);

        try {
            ActionConfig result =
                this.actionServlet.processActionConfigClass(customSub, this.moduleConfig);

            assertSame("The instance returned should be the param given it.",
                customSub, result);
        } catch (final ServletException e) {
            fail(e.getMessage());
        }
    }

    /**
     * Make sure the code throws the correct exception when it can't create an
     * instance of the base config's custom class.
     */
    @Test
    public void notestProcessActionConfigClassError() {
        ActionConfig customBase = new CustomActionConfigArg("/index");

        this.moduleConfig.addActionConfig(customBase);

        ActionConfig customSub = new ActionMapping();

        customSub.setPath("/sub");
        customSub.setExtends("/index");
        this.moduleConfig.addActionConfig(customSub);

        try {
            this.actionServlet.processActionConfigClass(customSub, this.moduleConfig);
            fail("Exception should be thrown");
        } catch (final UnavailableException e) {
            // success
            e.printStackTrace();
        } catch (final ServletException e) {
            fail("Unexpected exception thrown." + e);
        }
    }

    /**
     * Test the case where the subconfig has already specified its own config
     * class.  If the code still attempts to create a new instance, an error
     * will be thrown.
     */
    @Test
    public void testProcessActionConfigClassOverriddenSubConfigClass() {
        this.moduleConfig.addActionConfig(this.baseAction);

        ActionConfig customSub = new CustomActionConfigArg("/sub");

        customSub.setExtends("/index");
        this.moduleConfig.addActionConfig(customSub);

        try {
            this.actionServlet.processActionConfigClass(customSub, this.moduleConfig);
        } catch (final ServletException e) {
            fail("Exception should not be thrown" + e);
        }
    }

    /**
     * Used for testing custom FormBeanConfig classes.
     */
    public static class CustomFormBeanConfig extends FormBeanConfig {
        /** serialVersionUID */
        private static final long serialVersionUID = 5312111603155266154L;

        /** processExtendsCalled */
        private boolean processExtendsCalled = false;

        /** CustomFormBeanConfig */
        public CustomFormBeanConfig() {
            super();
        }

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

        /**
         * Set a flag so we know this method was called.
         */
        @Override
        public void processExtends(final Map<String, FormBeanConfig> moduleConfig) {
            this.processExtendsCalled = true;
        }
    }

    /**
     * Used to test cases where the subclass cannot be created with a no-arg
     * constructor.
     */
    private static class CustomFormBeanConfigArg extends FormBeanConfig {
        /** serialVersionUID */
        private static final long serialVersionUID = -455258948780624575L;

        CustomFormBeanConfigArg(final String name) {
            super();
            setName(name);
        }
    }

    /**
     * Used for testing custom ExceptionConfig classes.
     */
    public static class CustomExceptionConfig extends ExceptionConfig {
        /** serialVersionUID */
        private static final long serialVersionUID = -2722522988693737768L;

        /** processExtendsCalled */
        private boolean processExtendsCalled = false;

        /**
         * CustomExceptionConfig
         */
        public CustomExceptionConfig() {
            super();
        }

        /**
         * Set a flag so we know this method was called.
         */
        @Override
        public void processExtends(final Map<String, ExceptionConfig> moduleConfig,
                final Map<String, ExceptionConfig> actionConfig) {
            this.processExtendsCalled = true;
        }

        /**
         * @return processExtendsCalled
         */
        public boolean isProcessExtendsCalled() {
            return this.processExtendsCalled;
        }
    }

    /**
     * Used to test cases where the subclass cannot be created with a no-arg
     * constructor.
     */
    private static class CustomExceptionConfigArg extends ExceptionConfig {
        /** serialVersionUID */
        private static final long serialVersionUID = 4453990369781006902L;

        CustomExceptionConfigArg(final String type) {
            super();
            setType(type);
        }
    }

    /**
     * Used for testing custom ForwardConfig classes.
     */
    public static class CustomForwardConfig extends ForwardConfig {
        /** serialVersionUID */
        private static final long serialVersionUID = 5096162278387480886L;

        /** processExtendsCalled */
        private boolean processExtendsCalled = false;

        /**
         * CustomForwardConfig
         */
        public CustomForwardConfig() {
            super();
        }

        /**
         * @param name String
         * @param path String
         */
        public CustomForwardConfig(final String name, final String path) {
            super(name, path, false);
        }

        /**
         * Set a flag so we know this method was called.
         */
        @Override
        public void processExtends(final Map<String, ForwardConfig> moduleConfig,
                final Map<String, ForwardConfig> actionConfig) {
            this.processExtendsCalled = true;
        }

        /**
         * @return processExtendsCalled
         */
        public boolean isProcessExtendsCalled() {
            return this.processExtendsCalled;
        }
    }

    /**
     * Used to test cases where the subclass cannot be created with a no-arg
     * constructor.
     */
    private static class CustomForwardConfigArg extends ForwardConfig {
        /** serialVersionUID */
        private static final long serialVersionUID = -2880188065203315383L;

        CustomForwardConfigArg(final String name, final String path) {
            super();
            setName(name);
            setPath(path);
        }
    }

    /**
     * Used for testing custom ActionConfig classes.
     */
    public static class CustomActionConfig extends ActionConfig {
        /** serialVersionUID */
        private static final long serialVersionUID = -5608523390345631797L;

        /** processExtendsCalled */
        private boolean processExtendsCalled = false;

        /**
         * CustomActionConfig
         */
        public CustomActionConfig() {
            super();
        }

        /**
         * @param path String
         */
        public CustomActionConfig(final String path) {
            super();
            setPath(path);
        }

        /**
         * Set a flag so we know this method was called.
         */
        @Override
        public void processExtends(final Map<String, ActionConfig> moduleConfig) {
            this.processExtendsCalled = true;
        }

        /**
         * @return processExtendsCalled
         */
        public boolean isProcessExtendsCalled() {
            return this.processExtendsCalled;
        }
    }

    /**
     * Used to test cases where the subclass cannot be created with a no-arg
     * constructor.
     */
    private static class CustomActionConfigArg extends ActionConfig {
        /** serialVersionUID */
        private static final long serialVersionUID = 8225457345532365941L;

        CustomActionConfigArg(final String path) {
            super();
            setPath(path);
        }
    }

    // [...]
}
