You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by de...@apache.org on 2018/05/03 18:25:49 UTC

[myfaces-test] 02/21: copy shale test to myfaces

This is an automated email from the ASF dual-hosted git repository.

deki pushed a commit to branch 1_0_0_alpha
in repository https://gitbox.apache.org/repos/asf/myfaces-test.git

commit 2c106d1ab42f028706c511011559bfaca1d15098
Author: Matthias Wessendorf <ma...@apache.org>
AuthorDate: Thu May 21 20:41:48 2009 +0000

    copy shale test to myfaces
---
 shale-test/pom.xml                                 | 219 +++++
 .../shale/test/base/AbstractJsfTestCase.java       | 198 +++++
 .../test/base/AbstractViewControllerTestCase.java  |  97 +++
 .../apache/shale/test/cargo/CargoTestSetup.java    | 174 ++++
 .../org/apache/shale/test/config/ConfigParser.java | 480 +++++++++++
 .../java/org/apache/shale/test/config/package.html |  30 +
 .../apache/shale/test/el/AbstractELResolver.java   |  71 ++
 .../test/el/FacesImplicitObjectELResolver.java     | 272 +++++++
 .../test/el/FacesPropertyResolverChainWrapper.java | 233 ++++++
 .../test/el/FacesResourceBundleELResolver.java     | 212 +++++
 .../test/el/FacesScopedAttributeELResolver.java    | 241 ++++++
 .../test/el/FacesVariableResolverChainWrapper.java | 162 ++++
 .../org/apache/shale/test/el/MockELContext.java    | 124 +++
 .../shale/test/el/MockExpressionFactory.java       | 257 ++++++
 .../apache/shale/test/el/MockFunctionMapper.java   |  77 ++
 .../apache/shale/test/el/MockMethodExpression.java | 237 ++++++
 .../apache/shale/test/el/MockValueExpression.java  | 306 +++++++
 .../apache/shale/test/el/MockVariableMapper.java   |  79 ++
 .../shale/test/el/MockVariableValueExpression.java | 208 +++++
 .../java/org/apache/shale/test/el/package.html     |  21 +
 .../test/htmlunit/AbstractHtmlUnitTestCase.java    | 330 ++++++++
 .../shale/test/jmock/AbstractJmockJsfTestCase.java | 188 +++++
 .../apache/shale/test/mock/MockActionListener.java |  65 ++
 .../apache/shale/test/mock/MockApplication.java    | 463 +++++++++++
 .../apache/shale/test/mock/MockApplication12.java  | 274 +++++++
 .../shale/test/mock/MockApplicationFactory.java    | 103 +++
 .../apache/shale/test/mock/MockApplicationMap.java | 248 ++++++
 .../apache/shale/test/mock/MockEnumeration.java    |  79 ++
 .../shale/test/mock/MockExternalContext.java       | 476 +++++++++++
 .../shale/test/mock/MockExternalContext12.java     | 120 +++
 .../apache/shale/test/mock/MockFacesContext.java   | 324 ++++++++
 .../apache/shale/test/mock/MockFacesContext12.java | 114 +++
 .../shale/test/mock/MockFacesContextFactory.java   | 175 ++++
 .../shale/test/mock/MockHttpServletRequest.java    | 894 +++++++++++++++++++++
 .../shale/test/mock/MockHttpServletResponse.java   | 454 +++++++++++
 .../apache/shale/test/mock/MockHttpSession.java    | 338 ++++++++
 .../apache/shale/test/mock/MockMethodBinding.java  | 249 ++++++
 .../shale/test/mock/MockNavigationHandler.java     | 110 +++
 .../apache/shale/test/mock/MockPortletContext.java | 326 ++++++++
 .../apache/shale/test/mock/MockPortletRequest.java | 421 ++++++++++
 .../shale/test/mock/MockPortletResponse.java       |  62 ++
 .../apache/shale/test/mock/MockPortletSession.java | 268 ++++++
 .../org/apache/shale/test/mock/MockPrincipal.java  |  80 ++
 .../apache/shale/test/mock/MockPrintWriter.java    |  88 ++
 .../shale/test/mock/MockPropertyResolver.java      | 218 +++++
 .../org/apache/shale/test/mock/MockRenderKit.java  | 139 ++++
 .../shale/test/mock/MockRenderKitFactory.java      | 114 +++
 .../org/apache/shale/test/mock/MockRequestMap.java | 253 ++++++
 .../apache/shale/test/mock/MockResponseWriter.java | 371 +++++++++
 .../org/apache/shale/test/mock/MockServlet.java    | 122 +++
 .../apache/shale/test/mock/MockServletConfig.java  | 126 +++
 .../apache/shale/test/mock/MockServletContext.java | 479 +++++++++++
 .../shale/test/mock/MockServletOutputStream.java   |  96 +++
 .../org/apache/shale/test/mock/MockSessionMap.java | 254 ++++++
 .../apache/shale/test/mock/MockStateManager.java   | 113 +++
 .../apache/shale/test/mock/MockValueBinding.java   | 373 +++++++++
 .../shale/test/mock/MockVariableResolver.java      | 133 +++
 .../apache/shale/test/mock/MockViewHandler.java    | 152 ++++
 .../mock/lifecycle/ApplyRequestValuesExecutor.java |  38 +
 .../mock/lifecycle/DefaultRestoreViewSupport.java  | 114 +++
 .../mock/lifecycle/InvokeApplicationExecutor.java  |  38 +
 .../shale/test/mock/lifecycle/MockLifecycle.java   | 174 ++++
 .../test/mock/lifecycle/MockLifecycleFactory.java  |  89 ++
 .../shale/test/mock/lifecycle/PhaseExecutor.java   |  48 ++
 .../test/mock/lifecycle/PhaseListenerManager.java  | 100 +++
 .../mock/lifecycle/ProcessValidationsExecutor.java |  38 +
 .../mock/lifecycle/RenderResponseExecutor.java     |  50 ++
 .../test/mock/lifecycle/RestoreViewExecutor.java   | 168 ++++
 .../test/mock/lifecycle/RestoreViewSupport.java    |  72 ++
 .../mock/lifecycle/UpdateModelValuesExecutor.java  |  39 +
 shale-test/src/main/resources/META-INF/LICENSE.txt | 202 +++++
 shale-test/src/main/resources/META-INF/NOTICE.txt  |   9 +
 shale-test/src/site/site.xml                       |  83 ++
 shale-test/src/site/xdoc/index.xml                 | 286 +++++++
 .../shale/test/config/ConfigParserTestCase.java    | 158 ++++
 .../org/apache/shale/test/config/MyConverter.java  |  37 +
 .../org/apache/shale/test/config/MyRenderer.java   |  27 +
 .../test/el/MockExpressionFactoryTestCase.java     | 344 ++++++++
 .../shale/test/mock/MockObjectsTestCase.java       | 249 ++++++
 .../org/apache/shale/test/mock/TestMockBean.java   |  60 ++
 .../apache/shale/test/mock/ValueBindingTest.java   |  80 ++
 .../java/org/apache/shale/test/mock/data/Bean.java |  44 +
 .../apache/shale/test/config/faces-config-1.xml    |  73 ++
 83 files changed, 15510 insertions(+)

diff --git a/shale-test/pom.xml b/shale-test/pom.xml
new file mode 100644
index 0000000..89a98be
--- /dev/null
+++ b/shale-test/pom.xml
@@ -0,0 +1,219 @@
+<!--
+/*
+ * 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.
+ *
+ * $Id$
+ */
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.shale</groupId>
+        <artifactId>shale-parent</artifactId>
+        <version>1.1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>shale-test</artifactId>
+    <packaging>jar</packaging>
+    <name>Shale Test Framework</name>
+
+    <dependencies>
+
+        <!-- Required only for using the org.apache.shale.test.config package -->
+        <dependency>
+            <groupId>commons-digester</groupId>
+            <artifactId>commons-digester</artifactId>
+            <version>1.8</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>htmlunit</groupId>
+            <artifactId>htmlunit</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>jmock</groupId>
+            <artifactId>jmock</artifactId>
+            <version>1.0.1</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>jmock</groupId>
+            <artifactId>jmock-cglib</artifactId>
+            <version>1.0.1</version>
+            <optional>true</optional>
+        </dependency>        
+
+        <!-- For the "org.apache.shale.test.cargo" package, we need to have  -->
+        <!-- JUnit as a compile time dependency, not just scope="test".      -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>3.8.1</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.codehaus.cargo</groupId>
+            <artifactId>cargo-core-uberjar</artifactId>
+            <version>0.8</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.codehaus.cargo</groupId>
+            <artifactId>cargo-ant</artifactId>
+            <version>0.8</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.portlet</groupId>
+            <artifactId>portlet-api</artifactId>
+            <version>1.0</version>
+            <scope>provided</scope>
+        </dependency>
+
+    </dependencies>
+
+    <!-- Allow building with JDK 1.4 as well as JDK 1.5,
+         the 1.4 profile caters only to JSF 1.1 -->
+    <profiles>
+
+        <profile>
+            <id>shale-test-jdk14</id>
+            <activation>
+                <jdk>1.4</jdk>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>org.apache.myfaces.core</groupId>
+                    <artifactId>myfaces-api</artifactId>
+                    <version>1.1.4</version>
+                    <scope>provided</scope>
+                </dependency>
+                <dependency>
+                    <groupId>org.apache.myfaces.core</groupId>
+                    <artifactId>myfaces-impl</artifactId>
+                    <version>1.1.4</version>
+                    <scope>test</scope>
+                </dependency>
+                <dependency>
+                    <groupId>javax.servlet</groupId>
+                    <artifactId>servlet-api</artifactId>
+                    <version>2.4</version>
+                    <scope>provided</scope>
+                </dependency>
+                <dependency>
+                    <groupId>javax.servlet</groupId>
+                    <artifactId>jsp-api</artifactId>
+                    <version>2.0</version>
+                    <scope>provided</scope>
+                </dependency>
+            </dependencies>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-compiler-plugin</artifactId>
+                        <configuration>
+                            <excludes>
+                                <exclude>org/apache/shale/test/el/**</exclude>
+                                <exclude>org/apache/shale/test/mock/*12.java</exclude>
+                            </excludes>
+                            <testExcludes>
+                                <testExclude>org/apache/shale/test/el/**</testExclude>
+                            </testExcludes>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+
+        <profile>
+            <id>shale-test-jdk15</id>
+            <activation>
+                <jdk>1.5</jdk>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>javax.faces</groupId>
+                    <artifactId>jsf-api</artifactId>
+                    <version>1.2_03</version>
+                    <scope>provided</scope>
+                </dependency>
+                <dependency>
+                    <groupId>javax.faces</groupId>
+                    <artifactId>jsf-impl</artifactId>
+                    <version>1.2_03</version>
+                    <scope>test</scope>
+                </dependency>
+                <dependency>
+                    <groupId>javax.servlet</groupId>
+                    <artifactId>servlet-api</artifactId>
+                    <version>2.5</version>
+                    <scope>provided</scope>
+                </dependency>
+                <dependency>
+                    <groupId>javax.servlet.jsp</groupId>
+                    <artifactId>jsp-api</artifactId>
+                    <version>2.1</version>
+                    <scope>provided</scope>
+                </dependency>
+            </dependencies>
+        </profile>
+
+        <profile>
+            <id>shale-test-jdk16</id>
+            <activation>
+                <jdk>1.6</jdk>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>javax.faces</groupId>
+                    <artifactId>jsf-api</artifactId>
+                    <version>1.2_03</version>
+                    <scope>provided</scope>
+                </dependency>
+                <dependency>
+                    <groupId>javax.faces</groupId>
+                    <artifactId>jsf-impl</artifactId>
+                    <version>1.2_03</version>
+                    <scope>test</scope>
+                </dependency>
+                <dependency>
+                    <groupId>javax.servlet</groupId>
+                    <artifactId>servlet-api</artifactId>
+                    <version>2.5</version>
+                    <scope>provided</scope>
+                </dependency>
+                <dependency>
+                    <groupId>javax.servlet.jsp</groupId>
+                    <artifactId>jsp-api</artifactId>
+                    <version>2.1</version>
+                    <scope>provided</scope>
+                </dependency>
+            </dependencies>
+        </profile>
+
+    </profiles>
+
+</project>
diff --git a/shale-test/src/main/java/org/apache/shale/test/base/AbstractJsfTestCase.java b/shale-test/src/main/java/org/apache/shale/test/base/AbstractJsfTestCase.java
new file mode 100644
index 0000000..8f45cc7
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/base/AbstractJsfTestCase.java
@@ -0,0 +1,198 @@
+/*
+ * 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.shale.test.base;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+
+import javax.faces.FactoryFinder;
+import javax.faces.application.ApplicationFactory;
+import javax.faces.component.UIViewRoot;
+import javax.faces.lifecycle.LifecycleFactory;
+import javax.faces.render.RenderKitFactory;
+
+import junit.framework.TestCase;
+
+import org.apache.shale.test.mock.MockApplication;
+import org.apache.shale.test.mock.MockExternalContext;
+import org.apache.shale.test.mock.MockFacesContext;
+import org.apache.shale.test.mock.MockFacesContextFactory;
+import org.apache.shale.test.mock.MockHttpServletRequest;
+import org.apache.shale.test.mock.MockHttpServletResponse;
+import org.apache.shale.test.mock.MockHttpSession;
+import org.apache.shale.test.mock.MockRenderKit;
+import org.apache.shale.test.mock.MockServletConfig;
+import org.apache.shale.test.mock.MockServletContext;
+import org.apache.shale.test.mock.lifecycle.MockLifecycle;
+import org.apache.shale.test.mock.lifecycle.MockLifecycleFactory;
+
+/**
+ * <p>Abstract JUnit test case base class, which sets up the JavaServer Faces
+ * mock object environment for a particular simulated request.  The following
+ * protected variables are initialized in the <code>setUp()</code> method, and
+ * cleaned up in the <code>tearDown()</code> method:</p>
+ * <ul>
+ * <li><code>application</code> (<code>MockApplication</code>)</li>
+ * <li><code>config</code> (<code>MockServletConfig</code>)</li>
+ * <li><code>externalContext</code> (<code>MockExternalContext</code>)</li>
+ * <li><code>facesContext</code> (<code>MockFacesContext</code>)</li>
+ * <li><code>lifecycle</code> (<code>MockLifecycle</code>)</li>
+ * <li><code>request</code> (<code>MockHttpServletRequest</code></li>
+ * <li><code>response</code> (<code>MockHttpServletResponse</code>)</li>
+ * <li><code>servletContext</code> (<code>MockServletContext</code>)</li>
+ * <li><code>session</code> (<code>MockHttpSession</code>)</li>
+ * </ul>
+ *
+ * <p>In addition, appropriate factory classes will have been registered with
+ * <code>javax.faces.FactoryFinder</code> for <code>Application</code> and
+ * <code>RenderKit</code> instances.  The created <code>FacesContext</code>
+ * instance will also have been registered in the apppriate thread local
+ * variable, to simulate what a servlet container would do.</p>
+ *
+ * <p><strong>WARNING</strong> - If you choose to subclass this class, be sure
+ * your <code>setUp()</code> and <code>tearDown()</code> methods call
+ * <code>super.setUp()</code> and <code>super.tearDown()</code> respectively,
+ * and that you implement your own <code>suite()</code> method that exposes
+ * the test methods for your test case.</p>
+ */
+
+public abstract class AbstractJsfTestCase extends TestCase {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Construct a new instance of this test case.</p>
+     *
+     * @param name Name of this test case
+     */
+    public AbstractJsfTestCase(String name) {
+        super(name);
+    }
+
+
+    // ---------------------------------------------------- Overall Test Methods
+
+
+    /**
+     * <p>Set up instance variables required by this test case.</p>
+     */
+    protected void setUp() throws Exception {
+
+        // Set up a new thread context class loader
+        threadContextClassLoader = Thread.currentThread().getContextClassLoader();
+        Thread.currentThread().setContextClassLoader(new URLClassLoader(new URL[0],
+                this.getClass().getClassLoader()));
+
+        // Set up Servlet API Objects
+        servletContext = new MockServletContext();
+        config = new MockServletConfig(servletContext);
+        session = new MockHttpSession();
+        session.setServletContext(servletContext);
+        request = new MockHttpServletRequest(session);
+        request.setServletContext(servletContext);
+        response = new MockHttpServletResponse();
+
+        // Set up JSF API Objects
+        FactoryFinder.releaseFactories();
+        FactoryFinder.setFactory(FactoryFinder.APPLICATION_FACTORY,
+        "org.apache.shale.test.mock.MockApplicationFactory");
+        FactoryFinder.setFactory(FactoryFinder.FACES_CONTEXT_FACTORY,
+        "org.apache.shale.test.mock.MockFacesContextFactory");
+        FactoryFinder.setFactory(FactoryFinder.LIFECYCLE_FACTORY,
+        "org.apache.shale.test.mock.lifecycle.MockLifecycleFactory");
+        FactoryFinder.setFactory(FactoryFinder.RENDER_KIT_FACTORY,
+        "org.apache.shale.test.mock.MockRenderKitFactory");
+
+        externalContext =
+            new MockExternalContext(servletContext, request, response);
+        lifecycleFactory = (MockLifecycleFactory)
+        FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
+        lifecycle = (MockLifecycle)
+        lifecycleFactory.getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE);
+        facesContextFactory = (MockFacesContextFactory)
+        FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
+        facesContext = (MockFacesContext)
+        facesContextFactory.getFacesContext(servletContext,
+                request,
+                response,
+                lifecycle);
+        externalContext = (MockExternalContext) facesContext.getExternalContext();
+        UIViewRoot root = new UIViewRoot();
+        root.setViewId("/viewId");
+        root.setRenderKitId(RenderKitFactory.HTML_BASIC_RENDER_KIT);
+        facesContext.setViewRoot(root);
+        ApplicationFactory applicationFactory = (ApplicationFactory)
+          FactoryFinder.getFactory(FactoryFinder.APPLICATION_FACTORY);
+        application = (MockApplication) applicationFactory.getApplication();
+        facesContext.setApplication(application);
+        RenderKitFactory renderKitFactory = (RenderKitFactory)
+        FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
+        renderKit = new MockRenderKit();
+        renderKitFactory.addRenderKit(RenderKitFactory.HTML_BASIC_RENDER_KIT, renderKit);
+
+    }
+
+
+    /**
+     * <p>Tear down instance variables required by this test case.</p>
+     */
+    protected void tearDown() throws Exception {
+
+        application = null;
+        config = null;
+        externalContext = null;
+        facesContext.release();
+        facesContext = null;
+        lifecycle = null;
+        lifecycleFactory = null;
+        renderKit = null;
+        request = null;
+        response = null;
+        servletContext = null;
+        session = null;
+        FactoryFinder.releaseFactories();
+
+        Thread.currentThread().setContextClassLoader(threadContextClassLoader);
+        threadContextClassLoader = null;
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    // Mock object instances for our tests
+    protected MockApplication         application = null;
+    protected MockServletConfig       config = null;
+    protected MockExternalContext     externalContext = null;
+    protected MockFacesContext        facesContext = null;
+    protected MockFacesContextFactory facesContextFactory = null;
+    protected MockLifecycle           lifecycle = null;
+    protected MockLifecycleFactory    lifecycleFactory = null;
+    protected MockRenderKit           renderKit = null;
+    protected MockHttpServletRequest  request = null;
+    protected MockHttpServletResponse response = null;
+    protected MockServletContext      servletContext = null;
+    protected MockHttpSession         session = null;
+
+    // Thread context class loader saved and restored after each test
+    private ClassLoader threadContextClassLoader = null;
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/base/AbstractViewControllerTestCase.java b/shale-test/src/main/java/org/apache/shale/test/base/AbstractViewControllerTestCase.java
new file mode 100644
index 0000000..57a93d6
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/base/AbstractViewControllerTestCase.java
@@ -0,0 +1,97 @@
+/*
+ * 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.shale.test.base;
+
+import java.util.Iterator;
+
+/**
+ * <p>Abstract base class for testing <code>ViewController</code>
+ * implementations.</p>
+ *
+ * <p><strong>WARNING</strong> - If you choose to subclass this class, be sure
+ * your <code>setUp()</code> and <code>tearDown()</code> methods call
+ * <code>super.setUp()</code> and <code>super.tearDown()</code> respectively,
+ * and that you implement your own <code>suite()</code> method that exposes
+ * the test methods for your test case.</p>
+ */
+public abstract class AbstractViewControllerTestCase extends AbstractJsfTestCase {
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Construct a new instance of this test case.</p>
+     *
+     * @param name Test case name
+     */
+    public AbstractViewControllerTestCase(String name) {
+        super(name);
+    }
+
+
+    // ---------------------------------------------------- Overall Test Methods
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    // ------------------------------------------------------- Protected Methods
+
+
+    /**
+     * <p>Test that the specified number of messages have been queued on the
+     * <code>FacesContext</code> instance, without regard to matching a
+     * particular client identifier.</p>
+     *
+     * @param expected The expected number of messages
+     */
+    protected void checkMessageCount(int expected) {
+
+        int actual = 0;
+        Iterator messages = facesContext.getMessages();
+        while (messages.hasNext()) {
+            messages.next();
+            actual++;
+        }
+        assertEquals("Complete message count", expected, actual);
+
+    }
+
+
+    /**
+     * <p>Test that the specified number of messages have been queued on the
+     * <code>FacesContext</code> instance, for the specified client id.</p>
+     *
+     * @param clientId Client identifier of the component for which to
+     *  count queued messages
+     * @param expected The expected number of messages
+     */
+    protected void checkMessageCount(String clientId, int expected) {
+
+        int actual = 0;
+        Iterator messages = facesContext.getMessages(clientId);
+        while (messages.hasNext()) {
+            messages.next();
+            actual++;
+        }
+        assertEquals("Complete message count", expected, actual);
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/cargo/CargoTestSetup.java b/shale-test/src/main/java/org/apache/shale/test/cargo/CargoTestSetup.java
new file mode 100644
index 0000000..42d42b9
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/cargo/CargoTestSetup.java
@@ -0,0 +1,174 @@
+/*
+ * 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.shale.test.cargo;
+
+import java.io.File;
+
+import junit.extensions.TestSetup;
+import junit.framework.Test;
+
+import org.codehaus.cargo.container.InstalledLocalContainer;
+import org.codehaus.cargo.container.ContainerType;
+import org.codehaus.cargo.container.tomcat.Tomcat5xInstalledLocalContainer;
+import org.codehaus.cargo.container.deployable.Deployable;
+import org.codehaus.cargo.container.deployable.DeployableType;
+import org.codehaus.cargo.container.configuration.LocalConfiguration;
+import org.codehaus.cargo.container.configuration.ConfigurationType;
+import org.codehaus.cargo.generic.deployable.DefaultDeployableFactory;
+import org.codehaus.cargo.generic.configuration.ConfigurationFactory;
+import org.codehaus.cargo.generic.configuration.DefaultConfigurationFactory;
+import org.codehaus.cargo.generic.DefaultContainerFactory;
+import org.codehaus.cargo.util.log.FileLogger;
+
+/**
+ * <p>Convenience <code>TestSetup</code> class which uses Cargo to start
+ * and stop a Servlet container.</p>
+ */
+public class CargoTestSetup extends TestSetup {
+
+    // ------------------------------------------------------------ Constructors
+
+    /**
+     * <p>Construct a new instance of this test setup.</p>
+     *
+     * @param test Tests to be run within this test setup.
+     */
+    public CargoTestSetup(Test test) {
+        super(test);
+    }
+
+    // ------------------------------------------------------ Instance Variables
+
+    /**
+     * <p>The installed local container for this test setup.</p>
+     */
+    private InstalledLocalContainer container;
+
+    // ------------------------------------------------------ Test Setup Methods
+
+    /**
+     * <p>Start the container prior to running the tests.</p>
+     * <p>The following System properties are used:
+     * <ul>
+     * <li>cargo.container.id - ID of the container to use. [tomcat5x]</li>
+     * <li>cargo.container.home - Full path to a local installation of the container.
+     * If not set, uses the value of the TOMCAT_HOME environment variable.
+     * One of cargo.container.home or TOMCAT_HOME is REQUIRED.</li>
+     * <li>cargo.deployable - Full path to the war file to deploy. REQUIRED.</li>
+     * <li>cargo.container.output - Full path to a file to use for output. [none]</li>
+     * <li>cargo.container.log - Full path to a file to use for logging. [none]</li>
+     * <li>cargo.servlet.port - The port on which the container should listen. [8080]</li>
+     * </ul>
+     * </p>
+     *
+     * @throws Exception if an error occurs.
+     */
+    protected void setUp() throws Exception {
+
+        super.setUp();
+
+        // If there is no container id, default to Tomcat 5x
+        String containerId = System.getProperty("cargo.container.id");
+        if (containerId == null) {
+            containerId = Tomcat5xInstalledLocalContainer.ID;
+        }
+        System.out.println("[INFO] container id: " + containerId);
+
+        // Construct the war, using the container id and the path to the war file
+        String deployablePath = System.getProperty("cargo.deployable");
+        System.out.println("[INFO] deployable: " + deployablePath);
+        Deployable war = new DefaultDeployableFactory().createDeployable(
+                containerId,
+                deployablePath,
+                DeployableType.WAR);
+
+        // Container configuration
+        ConfigurationFactory configurationFactory =
+                new DefaultConfigurationFactory();
+
+        LocalConfiguration configuration =
+                (LocalConfiguration) configurationFactory.createConfiguration(
+                        containerId,
+                        ConfigurationType.STANDALONE);
+
+        // Find and (if provided) set the port to use for the container.
+        String servletPort = System.getProperty("cargo.servlet.port");
+        if (servletPort != null) {
+            configuration.setProperty("cargo.servlet.port", servletPort);
+            System.out.println("[INFO] servlet port: " + servletPort);
+        }
+
+        configuration.addDeployable(war);
+
+        container = (InstalledLocalContainer)
+                new DefaultContainerFactory().createContainer(
+                        containerId,
+                        ContainerType.INSTALLED, configuration);
+
+        // If 'cargo.container.home' is not set, or if an expression was
+        // passed through, try to use the TOMCAT_HOME environment variable.
+        String containerHome = System.getProperty("cargo.container.home");
+        if (containerHome == null || containerHome.startsWith("$")) {
+            containerHome = System.getenv("TOMCAT_HOME");
+        }
+        System.out.println("[INFO] container home: " + containerHome);
+        container.setHome(new File(containerHome));
+
+        // Find and (if provided) set the path to a log file
+        String containerLog = System.getProperty("cargo.container.log");
+        if (containerLog != null) {
+            System.out.println("[INFO] container log: " + containerLog);
+            container.setLogger(new FileLogger(containerLog, false));
+        }
+
+        // Find and (if provided) set the path to an output file
+        String containerOutput = System.getProperty("cargo.container.output");
+        if (containerOutput != null) {
+            System.out.println("[INFO] container output: " + containerOutput);
+            container.setOutput(new File(containerOutput));
+        }
+
+        container.start();
+    }
+
+
+    /**
+     * Stop the container after running the tests.
+     *
+     * @throws Exception if an error occurs.
+     */
+    protected void tearDown() throws Exception {
+        container.stop();
+        super.tearDown();
+    }
+
+
+    /**
+     * Return the name of the test setup.
+     * (Temporarily required due to MSUREFIRE-119.)
+     *
+     * @return the name of the test setup.
+     * @deprecated No replacement.
+     */
+
+    public String getName() {
+        return "CargoTestSetup";
+    }
+
+}
+
diff --git a/shale-test/src/main/java/org/apache/shale/test/config/ConfigParser.java b/shale-test/src/main/java/org/apache/shale/test/config/ConfigParser.java
new file mode 100644
index 0000000..b8e0dc4
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/config/ConfigParser.java
@@ -0,0 +1,480 @@
+/*
+ * 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.shale.test.config;
+
+import java.io.IOException;
+import java.net.URL;
+import javax.faces.FactoryFinder;
+import javax.faces.application.Application;
+import javax.faces.application.ApplicationFactory;
+import javax.faces.render.RenderKit;
+import javax.faces.render.RenderKitFactory;
+import javax.faces.render.Renderer;
+import org.apache.commons.digester.Digester;
+import org.apache.commons.digester.Rule;
+import org.apache.shale.test.mock.MockRenderKit;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * <p>Utility class to parse JavaServer Faces configuration resources, and
+ * register JSF artifacts with the mock object hierarchy.</p>
+ *
+ * <p>The following artifacts are registered:</p>
+ * <ul>
+ *     <li><code>Converter</code> (by-id and by-class)</li>
+ *     <li><code>RenderKit</code> and <code>Renderer</code></li>
+ *     <li><code>UIComponent</code></li>
+ *     <li><code>Validator</code></li>
+ * </ul>
+ *
+ * <p>Note that any declared <em>factory</em> instances are explicitly
+ * <strong>NOT</strong> registered, allowing the mock object hierarchy
+ * of the Shale Test Framework to manage these APIs.</p>
+ *
+ * <p><strong>USAGE NOTE</strong> - If you are using an instance of this
+ * class within a subclass of <code>AbstractJsfTestCase</code> or
+ * <code>AbstractJmockJsfTestCase</code>, be sure you have completed the
+ * <code>setUp()</code> processing in this base class before calling one
+ * of the <code>parse()</code> methods.</p>
+ *
+ * @since 1.1
+ */
+public final class ConfigParser {
+    
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /** Creates a new instance of ConfigParser */
+    public ConfigParser() {
+    }
+    
+
+    // ------------------------------------------------------ Manifest Constants
+
+
+    /**
+     * <p>Configuration resource URLs for the JSF RI.</p>
+     */
+    private static final String[] JSFRI_RESOURCES =
+    { "/com/sun/faces/jsf-ri-runtime.xml",
+    };
+
+
+    /**
+     * <p>Configuration resource URLs for Apache MyFaces.</p>
+     */
+    private static final String[] MYFACES_RESOURCES =
+    { "/org/apache/myfaces/resource/standard-faces-config.xml",
+    };
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>The <code>Digester</code> instance we will use for parsing.</p>
+     */
+    private Digester digester = null;
+
+
+    // ------------------------------------------------------- Public Properties
+
+
+    /**
+     * <p>Return the URLs of the platform configuration resources for this
+     * application.  The following platforms are currently supported:</p>
+     * <ul>
+     * <li>JavaServer Faces Reference Implementation (version 1.0 - 1.2)</li>
+     * <li>MyFaces (version 1.1)</li>
+     * </ul>
+     *
+     * <p>If MyFaces (version 1.2), currently under development, does not change
+     * the name of the configuration resource, it will be supported as well.</p>
+     */
+    public URL[] getPlatformURLs() {
+
+        URL[] urls = translate(JSFRI_RESOURCES);
+        if (urls[0] == null) {
+            urls = translate(MYFACES_RESOURCES);
+        }
+        return urls;
+
+    }
+
+
+    // ---------------------------------------------------------- Public Methods
+
+
+    /**
+     * <p>Parse the specified JavaServer Faces configuration resource, causing
+     * the appropriate JSF artifacts to be registered with the mock object
+     * hierarchy.</p>
+     *
+     * @param url <code>URL</code> of the configuration resource to parse
+     *
+     * @exception IOException if an input/output error occurs
+     * @exception SAXException if a parsing error occurs
+     */
+    public void parse(URL url) throws IOException, SAXException {
+
+        // Acquire and configure the Digester instance we will use
+        Digester digester = digester();
+        ApplicationFactory factory = (ApplicationFactory)
+          FactoryFinder.getFactory(FactoryFinder.APPLICATION_FACTORY);
+        Application application = factory.getApplication();
+        digester.push(application);
+
+        // Perform the required parsing
+        try {
+            digester.parse(url);
+        } finally {
+            digester.clear();
+        }
+
+    }
+
+
+    /**
+     * <p>Parse the specified set of JavaServer Faces configuration resources,
+     * in the listed order, causing the appropriate JSF artifacts to be registered
+     * with the mock object hierarchy.</p>
+     *
+     * @param urls <code>URL</code>s of the configuration resources to parse
+     *
+     * @exception IOException if an input/output error occurs
+     * @exception SAXException if a parsing error occurs
+     */
+    public void parse(URL[] urls) throws IOException, SAXException {
+
+        for (int i = 0; i < urls.length; i++) {
+            parse(urls[i]);
+        }
+
+    }
+
+
+    // --------------------------------------------------------- Private Methods
+
+
+    /**
+     * <p>Return the <code>Digester</code> instance we will use for parsing,
+     * creating and configuring a new instance if necessary.</p>
+     */
+    private Digester digester() {
+
+        if (this.digester == null) {
+            this.digester = new Digester();
+            digester.addRule("faces-config/component", new ComponentRule());
+            digester.addCallMethod
+              ("faces-config/component/component-type", "setComponentType", 0);
+            digester.addCallMethod
+              ("faces-config/component/component-class", "setComponentClass", 0);
+            digester.addRule("faces-config/converter", new ConverterRule());
+            digester.addCallMethod
+              ("faces-config/converter/converter-id", "setConverterId", 0);
+            digester.addCallMethod
+              ("faces-config/converter/converter-class", "setConverterClass", 0);
+            digester.addCallMethod
+              ("faces-config/converter/converter-for-class", "setConverterForClass", 0);
+            digester.addRule("faces-config/render-kit", new RenderKitRule());
+            digester.addRule("faces-config/render-kit/render-kit-id", new RenderKitIdRule());
+            digester.addRule("faces-config/render-kit/renderer", new RendererRule());
+            digester.addCallMethod
+              ("faces-config/render-kit/renderer/component-family", "setComponentFamily", 0);
+            digester.addCallMethod
+              ("faces-config/render-kit/renderer/renderer-class", "setRendererClass", 0);
+            digester.addCallMethod
+              ("faces-config/render-kit/renderer/renderer-type", "setRendererType", 0);
+            digester.addRule("faces-config/validator", new ValidatorRule());
+            digester.addCallMethod
+              ("faces-config/validator/validator-id", "setValidatorId", 0);
+            digester.addCallMethod
+              ("faces-config/validator/validator-class", "setValidatorClass", 0);
+        }
+        return this.digester;
+
+    }
+
+
+    /**
+     * <p>Translate an array of resource names into an array of resource URLs.</p>
+     *
+     * @param names Resource names to translate
+     */
+    private URL[] translate(String[] names) {
+
+        URL[] results = new URL[names.length];
+        for (int i = 0; i < names.length; i++) {
+            results[i] = this.getClass().getResource(names[i]);
+        }
+        return results;
+        
+    }
+
+
+    // --------------------------------------------------------- Private Classes
+
+
+    /**
+     * <p>Data bean that stores information related to a component.</p>
+     */
+    class ComponentBean {
+
+        private String componentClass;
+        public String getComponentClass() {
+            return this.componentClass;
+        }
+        public void setComponentClass(String componentClass) {
+            this.componentClass = componentClass;
+        }
+
+        private String componentType;
+        public String getComponentType() {
+            return this.componentType;
+        }
+        public void setComponentType(String componentType) {
+            this.componentType = componentType;
+        }
+
+    }
+
+
+    /**
+     * <p>Digester <code>Rule</code> for processing components.</p>
+     */
+    class ComponentRule extends Rule {
+
+        public void begin(String namespace, String name, Attributes attributes) {
+            getDigester().push(new ComponentBean());
+        }
+
+        public void end(String namespace, String name) {
+            ComponentBean bean = (ComponentBean) getDigester().pop();
+            Application application = (Application) getDigester().peek();
+            application.addComponent(bean.getComponentType(), bean.getComponentClass());
+        }
+
+    }
+
+
+    /**
+     * <p>Data bean that stores information related to a converter.</p>
+     */
+    class ConverterBean {
+
+        private String converterClass;
+        public String getConverterClass() {
+            return this.converterClass;
+        }
+        public void setConverterClass(String converterClass) {
+            this.converterClass = converterClass;
+        }
+
+        private String converterForClass;
+        public String getConverterForClass() {
+            return this.converterForClass;
+        }
+        public void setConverterForClass(String converterForClass) {
+            this.converterForClass = converterForClass;
+        }
+
+        private String converterId;
+        public String getConverterId() {
+            return this.converterId;
+        }
+        public void setConverterId(String converterId) {
+            this.converterId = converterId;
+        }
+
+    }
+
+
+    /**
+     * <p>Digester <code>Rule</code> for processing converers.</p>
+     */
+    class ConverterRule extends Rule {
+
+        public void begin(String namespace, String name, Attributes attributes) {
+            getDigester().push(new ConverterBean());
+        }
+
+        public void end(String namespace, String name) {
+            ConverterBean bean = (ConverterBean) getDigester().pop();
+            Application application = (Application) getDigester().peek();
+            if (bean.getConverterId() != null) {
+                application.addConverter(bean.getConverterId(), bean.getConverterClass());
+            } else {
+                Class clazz = null;
+                try {
+                    clazz = this.getClass().getClassLoader().loadClass(bean.getConverterForClass());
+                } catch (ClassNotFoundException e) {
+                    throw new IllegalArgumentException("java.lang.ClassNotFoundException: "
+                        + bean.getConverterForClass());
+                }
+                application.addConverter(clazz, bean.getConverterClass());
+            }
+        }
+
+    }
+
+
+    /**
+     * <p>Digester <code>Rule</code> for processing render kits.</p>
+     */
+    class RenderKitRule extends Rule {
+
+        public void begin(String namespace, String name, Attributes attributes) {
+            RenderKitFactory factory = (RenderKitFactory)
+              FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
+            getDigester().push(factory.getRenderKit(null, RenderKitFactory.HTML_BASIC_RENDER_KIT));
+        }
+
+        public void end(String namespace, String name) {
+            getDigester().pop();
+        }
+
+    }
+
+
+    /**
+     * <p>Digester <code>Rule</code> for processing render kit identifiers.</p>
+     */
+    class RenderKitIdRule extends Rule {
+
+        public void body(String namespace, String name, String text) {
+            String renderKitId = text.trim();
+            RenderKitFactory factory = (RenderKitFactory)
+              FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
+            RenderKit renderKit = factory.getRenderKit(null, renderKitId);
+            if (renderKit == null) {
+                renderKit = new MockRenderKit();
+                factory.addRenderKit(renderKitId, renderKit);
+            }
+            digester.pop();
+            digester.push(renderKit);
+        }
+
+    }
+
+
+    /**
+     * <p>Data bean that stores information related to a renderer.</p>
+     */
+    class RendererBean {
+
+        private String componentFamily;
+        public String getComponentFamily() {
+            return this.componentFamily;
+        }
+        public void setComponentFamily(String componentFamily) {
+            this.componentFamily = componentFamily;
+        }
+
+        private String rendererClass;
+        public String getRendererClass() {
+            return this.rendererClass;
+        }
+        public void setRendererClass(String rendererClass) {
+            this.rendererClass = rendererClass;
+        }
+
+        private String rendererType;
+        public String getRendererType() {
+            return this.rendererType;
+        }
+        public void setRendererType(String rendererType) {
+            this.rendererType = rendererType;
+        }
+
+    }
+
+
+    /**
+     * <p>Digester <code>Rule</code> for processing renderers.</p>
+     */
+    class RendererRule extends Rule {
+
+        public void begin(String namespace, String name, Attributes attributes) {
+            getDigester().push(new RendererBean());
+        }
+
+        public void end(String namespace, String name) {
+            RendererBean bean = (RendererBean) getDigester().pop();
+            RenderKit kit = (RenderKit) getDigester().peek();
+            Renderer renderer = null;
+            Class clazz = null;
+            try {
+                clazz = this.getClass().getClassLoader().loadClass(bean.getRendererClass());
+                renderer = (Renderer) clazz.newInstance();
+            } catch (Exception e) {
+                throw new IllegalArgumentException("Exception while trying to instantiate"
+                    + " renderer class '" + bean.getRendererClass() + "' : "
+                    + e.getMessage());
+            }
+            kit.addRenderer(bean.getComponentFamily(), bean.getRendererType(),
+                            renderer);
+        }
+
+    }
+
+
+    /**
+     * <p>Data bean that stores information related to a validator.</p>
+     */
+    class ValidatorBean {
+
+        private String validatorClass;
+        public String getValidatorClass() {
+            return this.validatorClass;
+        }
+        public void setValidatorClass(String validatorClass) {
+            this.validatorClass = validatorClass;
+        }
+
+        private String validatorId;
+        public String getValidatorId() {
+            return this.validatorId;
+        }
+        public void setValidatorId(String validatorId) {
+            this.validatorId = validatorId;
+        }
+
+    }
+
+
+    /**
+     * <p>Digester <code>Rule</code> for processing validators.</p>
+     */
+    class ValidatorRule extends Rule {
+
+        public void begin(String namespace, String name, Attributes attributes) {
+            getDigester().push(new ValidatorBean());
+        }
+
+        public void end(String namespace, String name) {
+            ValidatorBean bean = (ValidatorBean) getDigester().pop();
+            Application application = (Application) getDigester().peek();
+            application.addValidator(bean.getValidatorId(), bean.getValidatorClass());
+        }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/config/package.html b/shale-test/src/main/java/org/apache/shale/test/config/package.html
new file mode 100644
index 0000000..c73f764
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/config/package.html
@@ -0,0 +1,30 @@
+<body>
+<!--
+ 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.
+-->
+<p>This package contains a utility class,
+<a href="ConfigParser.html">ConfigParser</a>, which provides a
+<code>parse()</code> method that can parse JavaServer Faces configuration
+resources.  It supports the registration, in the mock object hierarchy
+set up by an instance of <code>AbstractJsfTestCase</code> or
+<code>AbstractJmockJsfTestCase</code>, of the following JSF artifacts:</p>
+<ul>
+    <li><code>Converter</code> (by-id and by-class)</li>
+    <li><code>RenderKit</code> and <code>Renderer</code></li>
+    <li><code>UIComponent</code></li>
+    <li><code>Validator</code></li>
+</ul>
+</body>
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/AbstractELResolver.java b/shale-test/src/main/java/org/apache/shale/test/el/AbstractELResolver.java
new file mode 100644
index 0000000..c3c9df2
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/AbstractELResolver.java
@@ -0,0 +1,71 @@
+/*
+ * 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.shale.test.el;
+
+import java.beans.FeatureDescriptor;
+import javax.el.ELResolver;
+
+/**
+ * <p>Convenience base class for EL resolvers.</p>
+ */
+abstract class AbstractELResolver extends ELResolver {
+    
+
+
+    // ------------------------------------------------------- Protected Methods
+
+
+    /**
+     * <p>Create and return a <code>FeatureDescriptor</code> configured with
+     * the specified arguments.</p>
+     *
+     * @param name Feature name
+     * @param displayName Display name
+     * @param description Short description
+     * @param expert Flag indicating this feature is for experts
+     * @param hidden Flag indicating this feature should be hidden
+     * @param preferred Flag indicating this feature is the preferred one
+     *  among features of the same type
+     * @param type Runtime type of this feature
+     * @param designTime Flag indicating feature is resolvable at design time
+     */
+    protected FeatureDescriptor descriptor(String name, String displayName,
+      String description, boolean expert, boolean hidden, boolean preferred,
+      Object type, boolean designTime) {
+
+      FeatureDescriptor descriptor = new FeatureDescriptor();
+
+      descriptor.setName(name);
+      descriptor.setDisplayName(displayName);
+      descriptor.setShortDescription(description);
+      descriptor.setExpert(expert);
+      descriptor.setHidden(hidden);
+      descriptor.setPreferred(preferred);
+      descriptor.setValue(ELResolver.TYPE, type);
+      if (designTime) {
+          descriptor.setValue(ELResolver.RESOLVABLE_AT_DESIGN_TIME, Boolean.TRUE);
+      } else {
+          descriptor.setValue(ELResolver.RESOLVABLE_AT_DESIGN_TIME, Boolean.FALSE);
+      }
+
+      return descriptor;
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/FacesImplicitObjectELResolver.java b/shale-test/src/main/java/org/apache/shale/test/el/FacesImplicitObjectELResolver.java
new file mode 100644
index 0000000..f79c989
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/FacesImplicitObjectELResolver.java
@@ -0,0 +1,272 @@
+/*
+ * 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.shale.test.el;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.el.ELContext;
+import javax.el.PropertyNotFoundException;
+import javax.el.PropertyNotWritableException;
+import javax.faces.component.UIViewRoot;
+import javax.faces.context.ExternalContext;
+import javax.faces.context.FacesContext;
+
+/**
+ * <p><code>ELResolver</code> implementation that accesses implicit objects
+ * in the current request context.  See the JSF 1.2 Specification, section
+ * 5.6.2.1, for requirements implemented by this class.</p>
+ *
+ * @since 1.0.4
+ */
+public class FacesImplicitObjectELResolver extends AbstractELResolver {
+    
+
+    /**
+     * <p>The names of all implicit objects recognized by this resolver.</p>
+     */
+    private static final String[] NAMES =
+    { "application", "applicationScope", "cookie", "facesContext",
+      "header", "headerValues", "initParam", "param", "paramValues",
+      "request", "requestScope", "session", "sessionScope", "view" };
+
+
+    /**
+     * <p>The property types corresponding to the implicit object names.</p>
+     */
+    private static final Class[] TYPES =
+    { Object.class, Map.class, Map.class, FacesContext.class,
+      Map.class, Map.class, Map.class, Map.class, Map.class,
+      Object.class, Map.class, Object.class, Map.class, UIViewRoot.class };
+
+
+    /**
+     * <p>The settable value types corresponding to the implicit
+     * object names.</p>
+     */
+    private static final Class[] VALUES =
+    { null, Object.class, null, null,
+      null, null, null, null, null,
+      null, Object.class, null, Object.class, null };
+
+
+    /**
+     * <p>Return the most general type this resolver accepts for the
+     * <code>property</code> argument.</p>
+     */
+    public Class getCommonPropertyType(ELContext context, Object base) {
+
+        if (base != null) {
+            return null;
+        } else {
+            return String.class;
+        }
+
+    }
+
+
+    /**
+     * <p>Return an <code>Iterator</code> over the attributes that this
+     * resolver knows how to deal with.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     */
+    public Iterator getFeatureDescriptors(ELContext context, Object base) {
+
+        if (base != null) {
+            return null;
+        }
+
+        // Create the variables we will need
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        List descriptors = new ArrayList();
+
+        // Add feature descriptors for each implicit object
+        for (int i = 0; i < NAMES.length; i++) {
+            descriptors.add(descriptor(NAMES[i], NAMES[i], NAMES[i],
+                                   false, false, true, TYPES[i], true));
+        }
+
+        // Return the accumulated descriptors
+        return descriptors.iterator();
+
+    }
+
+
+
+    /**
+     * <p>Return the Java type of the specified property.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public Class getType(ELContext context, Object base, Object property) {
+
+        if (base != null) {
+            return null;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+        String name = property.toString();
+        for (int i = 0; i < NAMES.length; i++) {
+            if (name.equals(NAMES[i])) {
+                context.setPropertyResolved(true);
+                return VALUES[i];
+            }
+        }
+        return null;
+
+    }
+
+
+    /**
+     * <p>Return an existing scoped object for the specified name (if any);
+     * otherwise, return <code>null</code>.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public Object getValue(ELContext context, Object base, Object property) {
+
+        if (base != null) {
+            return null;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ExternalContext econtext = fcontext.getExternalContext();
+        String name = property.toString();
+
+        if (name.equals("application")) {
+            context.setPropertyResolved(true);
+            return econtext.getContext();
+        } else if (name.equals("applicationScope")) {
+            context.setPropertyResolved(true);
+            return econtext.getApplicationMap();
+        } else if (name.equals("cookie")) {
+            context.setPropertyResolved(true);
+            return econtext.getRequestCookieMap();
+        } else if (name.equals("facesContext")) {
+            context.setPropertyResolved(true);
+            return fcontext;
+        } else if (name.equals("header")) {
+            context.setPropertyResolved(true);
+            return econtext.getRequestHeaderMap();
+        } else if (name.equals("headerValues")) {
+            context.setPropertyResolved(true);
+            return econtext.getRequestHeaderValuesMap();
+        } else if (name.equals("initParam")) {
+            context.setPropertyResolved(true);
+            return econtext.getInitParameterMap();
+        } else if (name.equals("param")) {
+            context.setPropertyResolved(true);
+            return econtext.getRequestParameterMap();
+        } else if (name.equals("paramValues")) {
+            context.setPropertyResolved(true);
+            return econtext.getRequestParameterValuesMap();
+        } else if (name.equals("request")) {
+            context.setPropertyResolved(true);
+            return econtext.getRequest();
+        } else if (name.equals("requestScope")) {
+            context.setPropertyResolved(true);
+            return econtext.getRequestMap();
+        } else if (name.equals("session")) {
+            context.setPropertyResolved(true);
+            return econtext.getSession(true);
+        } else if (name.equals("sessionScope")) {
+            context.setPropertyResolved(true);
+            return econtext.getSessionMap();
+        } else if (name.equals("view")) {
+            context.setPropertyResolved(true);
+            return fcontext.getViewRoot();
+        }
+
+        return null;
+
+    }
+
+
+    /**
+     * <p>Return <code>true</code> if the specified property is read only.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public boolean isReadOnly(ELContext context, Object base, Object property) {
+
+        if (base != null) {
+            return false;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+        String name = property.toString();
+        for (int i = 0; i < NAMES.length; i++) {
+            if (name.equals(NAMES[i])) {
+                context.setPropertyResolved(true);
+                return true;
+            }
+        }
+        return false;
+
+    }
+
+
+
+    /**
+     * <p>Set the value of a scoped object for the specified name.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     * @param value New value to be set
+     */
+    public void setValue(ELContext context, Object base, Object property, Object value) {
+
+        if (base != null) {
+            return;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+
+        String name = property.toString();
+        for (int i = 0; i < NAMES.length; i++) {
+            if (name.equals(NAMES[i])) {
+                context.setPropertyResolved(true);
+                throw new PropertyNotWritableException(name);
+            }
+        }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/FacesPropertyResolverChainWrapper.java b/shale-test/src/main/java/org/apache/shale/test/el/FacesPropertyResolverChainWrapper.java
new file mode 100644
index 0000000..27ff077
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/FacesPropertyResolverChainWrapper.java
@@ -0,0 +1,233 @@
+/*
+ * 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.shale.test.el;
+
+import java.util.Iterator;
+import java.util.List;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.faces.context.FacesContext;
+import javax.faces.el.EvaluationException;
+import javax.faces.el.PropertyResolver;
+
+/**
+ * <p><code>ELResolver</code> implementation that wraps the legacy (JSF 1.1)
+ * <code>PropertyResolver</code> chain.  See the JSF 1.2 Specification, section
+ * 5.6.1.6, for requirements implemented by this class.</p>
+ *
+ * @since 1.0.4
+ */
+public class FacesPropertyResolverChainWrapper extends AbstractELResolver {
+    
+
+    /**
+     * <p>Return the most general type this resolver accepts for the
+     * <code>property</code> argument.</p>
+     */
+    public Class getCommonPropertyType(ELContext context, Object base) {
+
+        if (base != null) {
+            return null;
+        } else {
+            return Object.class;
+        }
+
+    }
+
+
+    /**
+     * <p>Return an <code>Iterator</code> over the attributes that this
+     * resolver knows how to deal with.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     */
+    public Iterator getFeatureDescriptors(ELContext context, Object base) {
+
+        return null;
+
+    }
+
+
+
+    /**
+     * <p>Evaluate with the legacy property resolver chain and return
+     * the value.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public Class getType(ELContext context, Object base, Object property) {
+
+        if ((base == null) || (property == null)) {
+            return null;
+        }
+
+        context.setPropertyResolved(true);
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ELContext elContext = fcontext.getELContext();
+        PropertyResolver pr = fcontext.getApplication().getPropertyResolver();
+
+        if ((base instanceof List) || base.getClass().isArray()) {
+            Integer index = (Integer) fcontext.getApplication().getExpressionFactory().
+                    coerceToType(property, Integer.class);
+            try {
+                return pr.getType(base, index.intValue());
+            } catch (EvaluationException e) {
+                context.setPropertyResolved(false);
+                throw new ELException(e);
+            }
+        } else {
+            try {
+                return pr.getType(base, property);
+            } catch (EvaluationException e) {
+                context.setPropertyResolved(false);
+                throw new ELException(e);
+            }
+        }
+
+    }
+
+
+    /**
+     * <p>Evaluate with the legacy property resolver chain and return
+     * the value.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public Object getValue(ELContext context, Object base, Object property) {
+
+        if ((base == null) || (property == null)) {
+            return null;
+        }
+
+        context.setPropertyResolved(true);
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ELContext elContext = fcontext.getELContext();
+        PropertyResolver pr = fcontext.getApplication().getPropertyResolver();
+
+        if ((base instanceof List) || base.getClass().isArray()) {
+            Integer index = (Integer) fcontext.getApplication().getExpressionFactory().
+                    coerceToType(property, Integer.class);
+            try {
+                return pr.getValue(base, index.intValue());
+            } catch (EvaluationException e) {
+                context.setPropertyResolved(false);
+                throw new ELException(e);
+            }
+        } else {
+            try {
+                return pr.getValue(base, property);
+            } catch (EvaluationException e) {
+                context.setPropertyResolved(false);
+                throw new ELException(e);
+            }
+        }
+
+    }
+
+
+    /**
+     * <p>Return <code>true</code> if the specified property is read only.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public boolean isReadOnly(ELContext context, Object base, Object property) {
+
+        if ((base == null) || (property == null)) {
+            return false;
+        }
+
+        context.setPropertyResolved(true);
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ELContext elContext = fcontext.getELContext();
+        PropertyResolver pr = fcontext.getApplication().getPropertyResolver();
+
+        if ((base instanceof List) || base.getClass().isArray()) {
+            Integer index = (Integer) fcontext.getApplication().getExpressionFactory().
+                    coerceToType(property, Integer.class);
+            try {
+                return pr.isReadOnly(base, index.intValue());
+            } catch (EvaluationException e) {
+                context.setPropertyResolved(false);
+                throw new ELException(e);
+            }
+        } else {
+            try {
+                return pr.isReadOnly(base, property);
+            } catch (EvaluationException e) {
+                context.setPropertyResolved(false);
+                throw new ELException(e);
+            }
+        }
+
+    }
+
+
+
+    /**
+     * <p>Set the value of a property for the specified name.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     * @param value New value to be set
+     */
+    public void setValue(ELContext context, Object base, Object property, Object value) {
+
+        if ((base == null) || (property == null)) {
+            return;
+        }
+
+        context.setPropertyResolved(true);
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ELContext elContext = fcontext.getELContext();
+        PropertyResolver pr = fcontext.getApplication().getPropertyResolver();
+
+        if ((base instanceof List) || base.getClass().isArray()) {
+            Integer index = (Integer) fcontext.getApplication().getExpressionFactory().
+                    coerceToType(property, Integer.class);
+            try {
+                pr.setValue(base, index.intValue(), value);
+            } catch (EvaluationException e) {
+                context.setPropertyResolved(false);
+                throw new ELException(e);
+            }
+        } else {
+            try {
+                pr.setValue(base, property, value);
+            } catch (EvaluationException e) {
+                context.setPropertyResolved(false);
+                throw new ELException(e);
+            }
+        }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/FacesResourceBundleELResolver.java b/shale-test/src/main/java/org/apache/shale/test/el/FacesResourceBundleELResolver.java
new file mode 100644
index 0000000..7af0302
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/FacesResourceBundleELResolver.java
@@ -0,0 +1,212 @@
+/*
+ * 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.shale.test.el;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ResourceBundle;
+import java.util.Map.Entry;
+
+import javax.el.ELContext;
+import javax.el.PropertyNotFoundException;
+import javax.el.PropertyNotWritableException;
+import javax.faces.context.FacesContext;
+
+import org.apache.shale.test.mock.MockApplication12;
+
+/**
+ * <p><code>ELResolver</code> implementation that accesses resource bundles
+ * in the current application.  See the JSF 1.2 Specification, section
+ * 5.6.1.3, for requirements implemented by this class.</p>
+ *
+ * @since 1.0.4
+ */
+public class FacesResourceBundleELResolver extends AbstractELResolver {
+    
+
+    /**
+     * <p>Return the most general type this resolver accepts for the
+     * <code>property</code> argument.</p>
+     */
+    public Class getCommonPropertyType(ELContext context, Object base) {
+
+        if (base != null) {
+            return null;
+        } else {
+            return String.class;
+        }
+
+    }
+
+
+    /**
+     * <p>Return an <code>Iterator</code> over the attributes that this
+     * resolver knows how to deal with.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     */
+    public Iterator getFeatureDescriptors(ELContext context, Object base) {
+
+        if (base != null) {
+            return null;
+        }
+
+        // Create the variables we will need
+        List descriptors = new ArrayList();
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        MockApplication12 application =
+                (MockApplication12) fcontext.getApplication();
+        String key = null;
+        Object value = null;
+
+        // Create a feature descriptor for each configured resource bundle
+        Iterator entries = application.getResourceBundles().entrySet().iterator();
+        while (entries.hasNext()) {
+            Entry entry = (Entry) entries.next();
+            key = (String) entry.getKey();
+            value = entry.getValue();
+            descriptors.add(descriptor(key, key, "Resource Bundle " + key,
+                                       false, false, true,
+                                       ResourceBundle.class, true));
+        }
+
+        // Return the accumulated descriptors
+        return descriptors.iterator();
+
+    }
+
+
+    /**
+     * <p>Return the Java type of the specified property.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public Class getType(ELContext context, Object base, Object property) {
+
+        if (base != null) {
+            return null;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ResourceBundle bundle =
+          fcontext.getApplication().getResourceBundle(fcontext, property.toString());
+        if (bundle != null) {
+            context.setPropertyResolved(true);
+            return ResourceBundle.class;
+        }
+        return null;
+
+    }
+
+
+    /**
+     * <p>Return a resource bundle for the specified name (if any);
+     * otherwise, return <code>null</code>.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public Object getValue(ELContext context, Object base, Object property) {
+
+        if (base != null) {
+            return null;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ResourceBundle bundle =
+          fcontext.getApplication().getResourceBundle(fcontext, property.toString());
+        if (bundle != null) {
+            context.setPropertyResolved(true);
+            return bundle;
+        }
+        return null;
+
+    }
+
+
+    /**
+     * <p>Return <code>true</code> if the specified property is read only.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public boolean isReadOnly(ELContext context, Object base, Object property) {
+
+        if (base != null) {
+            return false;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ResourceBundle bundle =
+          fcontext.getApplication().getResourceBundle(fcontext, property.toString());
+        if (bundle != null) {
+            context.setPropertyResolved(true);
+            return true;
+        }
+        return false;
+
+    }
+
+
+
+    /**
+     * <p>Set the value of a scoped object for the specified name.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     * @param value New value to be set
+     */
+    public void setValue(ELContext context, Object base, Object property, Object value) {
+
+        if (base != null) {
+            return;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ResourceBundle bundle =
+          fcontext.getApplication().getResourceBundle(fcontext, property.toString());
+        if (bundle != null) {
+            context.setPropertyResolved(true);
+            throw new PropertyNotWritableException(property.toString());
+        }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/FacesScopedAttributeELResolver.java b/shale-test/src/main/java/org/apache/shale/test/el/FacesScopedAttributeELResolver.java
new file mode 100644
index 0000000..a9a5e23
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/FacesScopedAttributeELResolver.java
@@ -0,0 +1,241 @@
+/*
+ * 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.shale.test.el;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import javax.el.ELContext;
+import javax.el.PropertyNotFoundException;
+import javax.faces.context.ExternalContext;
+import javax.faces.context.FacesContext;
+
+/**
+ * <p><code>ELResolver</code> implementation that accesses scoped variables
+ * in the current request context.  See the JSF 1.2 Specification, section
+ * 5.6.2.7, for requirements implemented by this class.</p>
+ *
+ * @since 1.0.4
+ */
+public class FacesScopedAttributeELResolver extends AbstractELResolver {
+    
+
+    /**
+     * <p>Return the most general type this resolver accepts for the
+     * <code>property</code> argument.</p>
+     */
+    public Class getCommonPropertyType(ELContext context, Object base) {
+
+        if (base != null) {
+            return null;
+        } else {
+            return String.class;
+        }
+
+    }
+
+
+    /**
+     * <p>Return an <code>Iterator</code> over the attributes that this
+     * resolver knows how to deal with.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     */
+    public Iterator getFeatureDescriptors(ELContext context, Object base) {
+
+        if (base != null) {
+            return null;
+        }
+
+        // Create the variables we will need
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ExternalContext econtext = fcontext.getExternalContext();
+        List descriptors = new ArrayList();
+        Map map = null;
+        Set set = null;
+        Iterator items = null;
+        String key = null;
+        Object value = null;
+
+        // Add feature descriptors for request scoped attributes
+        set = econtext.getRequestMap().entrySet();
+        items = set.iterator();
+        while (items.hasNext()) {
+            Entry item = (Entry) items.next();
+            key = (String) item.getKey();
+            value = item.getValue();
+            descriptors.add(descriptor(key, key, "Request Scope Attribute " + key,
+                                       false, false, true, value.getClass(), true));
+        }
+
+        // Add feature descriptors for session scoped attributes
+        set = econtext.getSessionMap().entrySet();
+        items = set.iterator();
+        while (items.hasNext()) {
+            Entry item = (Entry) items.next();
+            key = (String) item.getKey();
+            value = item.getValue();
+            descriptors.add(descriptor(key, key, "Session Scope Attribute " + key,
+                                       false, false, true, value.getClass(), true));
+        }
+
+        // Add feature descriptors for application scoped attributes
+        set = econtext.getApplicationMap().entrySet();
+        items = set.iterator();
+        while (items.hasNext()) {
+            Entry item = (Entry) items.next();
+            key = (String) item.getKey();
+            value = item.getValue();
+            descriptors.add(descriptor(key, key, "Application Scope Attribute " + key,
+                                       false, false, true, value.getClass(), true));
+        }
+
+        // Return the accumulated descriptors
+        return descriptors.iterator();
+
+    }
+
+
+
+    /**
+     * <p>Return the Java type of the specified property.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public Class getType(ELContext context, Object base, Object property) {
+
+        if (base != null) {
+            return null;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+        context.setPropertyResolved(true);
+        return Object.class;
+
+    }
+
+
+    /**
+     * <p>Return an existing scoped object for the specified name (if any);
+     * otherwise, return <code>null</code>.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public Object getValue(ELContext context, Object base, Object property) {
+
+        if (base != null) {
+            return null;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ExternalContext econtext = fcontext.getExternalContext();
+        Object value = null;
+        value = econtext.getRequestMap().get(property);
+        if (value != null) {
+            context.setPropertyResolved(true);
+            return value;
+        }
+        value = econtext.getSessionMap().get(property);
+        if (value != null) {
+            context.setPropertyResolved(true);
+            return value;
+        }
+        value = econtext.getApplicationMap().get(property);
+        if (value != null) {
+            context.setPropertyResolved(true);
+            return value;
+        }
+
+        return null;
+
+    }
+
+
+    /**
+     * <p>Return <code>true</code> if the specified property is read only.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public boolean isReadOnly(ELContext context, Object base, Object property) {
+
+        if (base == null) {
+            context.setPropertyResolved(true);
+            return false;
+        }
+        return false;
+
+    }
+
+
+
+    /**
+     * <p>Set the value of a scoped object for the specified name.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     * @param value New value to be set
+     */
+    public void setValue(ELContext context, Object base, Object property, Object value) {
+
+        if (base != null) {
+            return;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+
+        context.setPropertyResolved(true);
+        String key = property.toString();
+        Object result = null;
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ExternalContext econtext = fcontext.getExternalContext();
+
+        if (econtext.getRequestMap().containsKey(property)) {
+            econtext.getRequestMap().put(key, value);
+        } else if (econtext.getSessionMap().containsKey(property)) {
+            econtext.getSessionMap().put(key, value);
+        } else if (econtext.getApplicationMap().containsKey(property)) {
+            econtext.getApplicationMap().put(key, value);
+        } else {
+            econtext.getRequestMap().put(key, value);
+        }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/FacesVariableResolverChainWrapper.java b/shale-test/src/main/java/org/apache/shale/test/el/FacesVariableResolverChainWrapper.java
new file mode 100644
index 0000000..aa22068
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/FacesVariableResolverChainWrapper.java
@@ -0,0 +1,162 @@
+/*
+ * 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.shale.test.el;
+
+import java.util.Iterator;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.PropertyNotFoundException;
+import javax.faces.context.ExternalContext;
+import javax.faces.context.FacesContext;
+import javax.faces.el.VariableResolver;
+
+/**
+ * <p><code>ELResolver</code> implementation that wraps the legacy (JSF 1.1)
+ * <code>VariableResolver</code> chain.  See the JSF 1.2 Specification, section
+ * 5.6.1.5, for requirements implemented by this class.</p>
+ *
+ * @since 1.0.4
+ */
+public class FacesVariableResolverChainWrapper extends AbstractELResolver {
+    
+
+    /**
+     * <p>Return the most general type this resolver accepts for the
+     * <code>property</code> argument.</p>
+     */
+    public Class getCommonPropertyType(ELContext context, Object base) {
+
+        if (base != null) {
+            return null;
+        } else {
+            return String.class;
+        }
+
+    }
+
+
+    /**
+     * <p>Return an <code>Iterator</code> over the attributes that this
+     * resolver knows how to deal with.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     */
+    public Iterator getFeatureDescriptors(ELContext context, Object base) {
+
+        return null;
+
+    }
+
+
+
+    /**
+     * <p>Return the Java type of the specified property.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public Class getType(ELContext context, Object base, Object property) {
+
+        if ((base == null) && (property == null)) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+        return null;
+
+    }
+
+
+    /**
+     * <p>Evaluate with the legacy variable resolver chain and return
+     * the value.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public Object getValue(ELContext context, Object base, Object property) {
+
+        if (base != null) {
+            return null;
+        }
+        if (property == null) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ExternalContext econtext = fcontext.getExternalContext();
+        String name = property.toString();
+
+        ELContext elContext = fcontext.getELContext();
+        VariableResolver vr = fcontext.getApplication().getVariableResolver();
+        try {
+            Object value = vr.resolveVariable(fcontext, name);
+            if (value != null) {
+                context.setPropertyResolved(true);
+            }
+            return value;
+        } catch (Exception e) {
+            context.setPropertyResolved(false);
+            throw new ELException(e);
+        }
+
+    }
+
+
+    /**
+     * <p>Return <code>true</code> if the specified property is read only.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     */
+    public boolean isReadOnly(ELContext context, Object base, Object property) {
+
+        if ((base == null) && (property == null)) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+        return false;
+
+    }
+
+
+
+    /**
+     * <p>Set the value of a scoped object for the specified name.</p>
+     *
+     * @param context <code>ELContext</code> for evaluating this value
+     * @param base Base object against which this evaluation occurs
+     *  (must be null because we are evaluating a top level variable)
+     * @param property Property name to be accessed
+     * @param value New value to be set
+     */
+    public void setValue(ELContext context, Object base, Object property, Object value) {
+
+        if ((base == null) && (property == null)) {
+            throw new PropertyNotFoundException("No property specified");
+        }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/MockELContext.java b/shale-test/src/main/java/org/apache/shale/test/el/MockELContext.java
new file mode 100644
index 0000000..237499d
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/MockELContext.java
@@ -0,0 +1,124 @@
+/*
+ * 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.shale.test.el;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import javax.el.ELContext;
+import javax.el.ELResolver;
+import javax.el.FunctionMapper;
+import javax.el.VariableMapper;
+import javax.faces.context.FacesContext;
+
+/**
+ * <p>Mock implementation of <code>ELContext</code>.</p>
+ *
+ * @since 1.0.4
+ */
+
+public class MockELContext extends ELContext {
+    
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /** Creates a new instance of MockELContext */
+    public MockELContext() {
+    }
+    
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    private Map contexts = new HashMap();
+    private FunctionMapper functionMapper = new MockFunctionMapper();
+    private Locale locale = Locale.getDefault();
+    private boolean propertyResolved;
+    private VariableMapper variableMapper = new MockVariableMapper();
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+
+    // ------------------------------------------------------- ELContext Methods
+
+
+    /** {@inheritDoc} */
+    public Object getContext(Class key) {
+        if (key == null) {
+            throw new NullPointerException();
+        }
+        return contexts.get(key);
+    }
+
+
+    /** {@inheritDoc} */
+    public ELResolver getELResolver() {
+        FacesContext context = FacesContext.getCurrentInstance();
+        return context.getApplication().getELResolver();
+    }
+
+
+    /** {@inheritDoc} */
+    public FunctionMapper getFunctionMapper() {
+        return this.functionMapper;
+    }
+
+
+    /** {@inheritDoc} */
+    public Locale getLocale() {
+        return this.locale;
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isPropertyResolved() {
+        return this.propertyResolved;
+    }
+
+
+    /** {@inheritDoc} */
+    public void putContext(Class key, Object value) {
+        if ((key == null) || (value == null)) {
+            throw new NullPointerException();
+        }
+        contexts.put(key, value);
+    }
+
+
+    /** {@inheritDoc} */
+    public void setPropertyResolved(boolean propertyResolved) {
+        this.propertyResolved = propertyResolved;
+    }
+
+
+    /** {@inheritDoc} */
+    public VariableMapper getVariableMapper() {
+        return this.variableMapper;
+    }
+
+
+    /** {@inheritDoc} */
+    public void setLocale(Locale locale) {
+        this.locale = locale;
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/MockExpressionFactory.java b/shale-test/src/main/java/org/apache/shale/test/el/MockExpressionFactory.java
new file mode 100644
index 0000000..e973008
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/MockExpressionFactory.java
@@ -0,0 +1,257 @@
+/*
+ * 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.shale.test.el;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import javax.el.ELContext;
+import javax.el.ExpressionFactory;
+import javax.el.MethodExpression;
+import javax.el.ValueExpression;
+
+/**
+ * <p>Mock implementation of <code>ExpressionFactory</code>.</p>
+ *
+ * @since 1.0.4
+ */
+public class MockExpressionFactory extends ExpressionFactory {
+    
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /** Creates a new instance of MockExpressionFactory */
+    public MockExpressionFactory() {
+    }
+    
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>Literal numeric value for zero.</p>
+     */
+    private static final Integer ZERO = new Integer(0);
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+
+    // ----------------------------------------------- ExpressionFactory Methods
+
+
+    /** {@inheritDoc} */
+    public Object coerceToType(Object object, Class targetType) {
+
+        // Check for no conversion necessary
+        if ((targetType == null) || Object.class.equals(targetType)) {
+            return object;
+        }
+
+        // Coerce to String if appropriate
+        if (String.class.equals(targetType)) {
+            if (object == null) {
+                return "";
+            } else if (object instanceof String) {
+                return (String) object;
+            } else {
+                return object.toString();
+            }
+        }
+
+        // Coerce to Number (or a subclass of Number) if appropriate
+        if (isNumeric(targetType)) {
+            if (object == null) {
+                return coerce(ZERO, targetType);
+            } else if ("".equals(object)) {
+                return coerce(ZERO, targetType);
+            } else if (object instanceof String) {
+                return coerce((String) object, targetType);
+            } else if (isNumeric(object.getClass())) {
+                return coerce((Number) object, targetType);
+            }
+            throw new IllegalArgumentException("Cannot convert " + object + " to Number");
+        }
+
+        // Coerce to Boolean if appropriate
+        if (Boolean.class.equals(targetType) || (Boolean.TYPE == targetType)) {
+            if (object == null) {
+                return Boolean.FALSE;
+            } else if ("".equals(object)) {
+                return Boolean.FALSE;
+            } else if ((object instanceof Boolean) || (object.getClass() == Boolean.TYPE)) {
+                return (Boolean) object;
+            } else if (object instanceof String) {
+                return Boolean.valueOf((String) object);
+            }
+            throw new IllegalArgumentException("Cannot convert " + object + " to Boolean");
+        }
+
+        // Coerce to Character if appropriate
+        if (Character.class.equals(targetType) || (Character.TYPE == targetType)) {
+            if (object == null) {
+                return new Character((char) 0);
+            } else if ("".equals(object)) {
+                return new Character((char) 0);
+            } else if (object instanceof String) {
+                return new Character(((String) object).charAt(0));
+            } else if (isNumeric(object.getClass())) {
+                return new Character((char) ((Number) object).shortValue());
+            } else if ((object instanceof Character) || (object.getClass() == Character.TYPE)) {
+                return (Character) object;
+            }
+            throw new IllegalArgumentException("Cannot convert " + object + " to Character");
+        }
+
+        // Is the specified value type-compatible already?
+        if ((object != null) && targetType.isAssignableFrom(object.getClass())) {
+            return object;
+        }
+
+        // We do not know how to perform this conversion
+        throw new IllegalArgumentException("Cannot convert " + object + " to " + targetType.getName());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public MethodExpression createMethodExpression(ELContext context,
+                                                   String expression,
+                                                   Class expectedType,
+                                                   Class[] signature) {
+
+        return new MockMethodExpression(expression, signature, expectedType);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ValueExpression createValueExpression(ELContext context,
+                                                 String expression,
+                                                 Class expectedType) {
+
+        return new MockValueExpression(expression, expectedType);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ValueExpression createValueExpression(Object instance,
+                                                 Class expectedType) {
+
+        return new MockVariableValueExpression(instance, expectedType);
+
+    }
+
+
+    // --------------------------------------------------------- Private Methods
+
+
+    /**
+     * <p>Coerce the specified value to the specified Number subclass.</p>
+     *
+     * @param value Value to be coerced
+     * @param type Destination type
+     */
+    private Number coerce(Number value, Class type) {
+
+        if ((type == Byte.TYPE) || (type == Byte.class)) {
+            return new Byte(value.byteValue());
+        } else if ((type == Double.TYPE) || (type == Double.class)) {
+            return new Double(value.doubleValue());
+        } else if ((type == Float.TYPE) || (type == Float.class)) {
+            return new Float(value.floatValue());
+        } else if ((type == Integer.TYPE) || (type == Integer.class)) {
+            return new Integer(value.intValue());
+        } else if ((type == Long.TYPE) || (type == Long.class)) {
+            return new Long(value.longValue());
+        } else if ((type == Short.TYPE) || (type == Short.class)) {
+            return new Short(value.shortValue());
+        } else if (type == BigDecimal.class) {
+            if (value instanceof BigDecimal) {
+                return (BigDecimal) value;
+            } else if (value instanceof BigInteger) {
+                return new BigDecimal((BigInteger) value);
+            } else {
+                return new BigDecimal(((Number) value).doubleValue());
+            }
+        } else if (type == BigInteger.class) {
+            if (value instanceof BigInteger) {
+                return (BigInteger) value;
+            } else if (value instanceof BigDecimal) {
+                return ((BigDecimal) value).toBigInteger();
+            } else {
+                return BigInteger.valueOf(((Number) value).longValue());
+            }
+        }
+        throw new IllegalArgumentException("Cannot convert " + value + " to " + type.getName());
+
+    }
+
+
+    /**
+     * <p>Coerce the specified value to the specified Number subclass.</p>
+     *
+     * @param value Value to be coerced
+     * @param type Destination type
+     */
+    private Number coerce(String value, Class type) {
+
+        if ((type == Byte.TYPE) || (type == Byte.class)) {
+            return Byte.valueOf(value);
+        } else if ((type == Double.TYPE) || (type == Double.class)) {
+            return Double.valueOf(value);
+        } else if ((type == Float.TYPE) || (type == Float.class)) {
+            return Float.valueOf(value);
+        } else if ((type == Integer.TYPE) || (type == Integer.class)) {
+            return Integer.valueOf(value);
+        } else if ((type == Long.TYPE) || (type == Long.class)) {
+            return Long.valueOf(value);
+        } else if ((type == Short.TYPE) || (type == Short.class)) {
+            return Short.valueOf(value);
+        } else if (type == BigDecimal.class) {
+            return new BigDecimal(value);
+        } else if (type == BigInteger.class) {
+            return new BigInteger(value);
+        }
+        throw new IllegalArgumentException("Cannot convert " + value + " to " + type.getName());
+
+    }
+
+
+    /**
+     * <p>Return <code>true</code> if the specified type is numeric.</p>
+     *
+     * @param type Type to check
+     */
+    private boolean isNumeric(Class type) {
+
+        return
+               (type == Byte.TYPE) || (type == Byte.class)
+            || (type == Double.TYPE) || (type == Double.class)
+            || (type == Float.TYPE) || (type == Float.class)
+            || (type == Integer.TYPE) || (type == Integer.class)
+            || (type == Long.TYPE) || (type == Long.class)
+            || (type == Short.TYPE) || (type == Short.class)
+            || (type == BigDecimal.class) || (type == BigInteger.class);
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/MockFunctionMapper.java b/shale-test/src/main/java/org/apache/shale/test/el/MockFunctionMapper.java
new file mode 100644
index 0000000..1bed5a2
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/MockFunctionMapper.java
@@ -0,0 +1,77 @@
+/*
+ * 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.shale.test.el;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import javax.el.FunctionMapper;
+
+/**
+ * <p>Mock implementation of <code>FunctionMapper</code>.</p>
+ *
+ * @since 1.0.4
+ */
+
+public class MockFunctionMapper extends FunctionMapper {
+    
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /** Creates a new instance of MockFunctionMapper */
+    public MockFunctionMapper() {
+    }
+    
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>Map of <code>Method</code> descriptors for static methods, keyed by
+     * a string composed of the prefix (or "" if none), a ":", and the local name.</p>
+     */
+    private Map functions = new HashMap();
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    /**
+     * <p>Store a mapping of the specified prefix and localName to the
+     * specified method, which must be static.</p>
+     */
+    public void mapFunction(String prefix, String localName, Method method) {
+
+        functions.put(prefix + ":" + localName, method);
+
+    }
+
+
+    // -------------------------------------------------- FunctionMapper Methods
+
+
+    /** {@inheritDoc} */
+    public Method resolveFunction(String prefix, String localName) {
+
+        return (Method) functions.get(prefix + ":" + localName);
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/MockMethodExpression.java b/shale-test/src/main/java/org/apache/shale/test/el/MockMethodExpression.java
new file mode 100644
index 0000000..ab9e970
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/MockMethodExpression.java
@@ -0,0 +1,237 @@
+/*
+ * 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.shale.test.el;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ELResolver;
+import javax.el.MethodExpression;
+import javax.el.MethodInfo;
+import javax.faces.context.FacesContext;
+
+/**
+ * <p>Mock implementation of <code>MethodExpression</code>.</p>
+ */
+public class MockMethodExpression extends MethodExpression {
+    
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * Serial version UID.
+     */
+    private static final long serialVersionUID = 5694105394290316715L;
+
+
+    /**
+     * <p>Construct a new expression for the specified expression string.</p>
+     *
+     * @param expression Expression string to be evaluated
+     * @param signature Parameter signature of the method to be called
+     * @param expectedType Expected type of the result
+     */
+    public MockMethodExpression(String expression, Class[] signature, Class expectedType) {
+
+        if (expression == null) {
+            throw new NullPointerException("Expression string cannot be null");
+        }
+        this.expression = expression;
+        this.signature = signature;
+        this.expectedType = expectedType;
+        parse();
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>The parsed elements of this expression.</p>
+     */
+    private String[] elements = null;
+
+
+    /**
+     * <p>The expected result type for <code>getValue()</code> calls.</p>
+     */
+    private Class expectedType = null;
+
+
+    /**
+     * <p>The original expression string used to create this expression.</p>
+     */
+    private String expression = null;
+
+
+    /**
+     * <p>The method signature of the method to be called.</p>
+     */
+    private Class[] signature = null;
+
+
+    // ------------------------------------------------------ Expression Methods
+
+
+    /**
+     * <p>Return <code>true</code> if this expression is equal to the
+     * specified expression.</p>
+     *
+     * @param obj Object to be compared
+     */
+    public boolean equals(Object obj) {
+
+        if ((obj != null) & (obj instanceof MethodExpression)) {
+            return expression.equals(((MethodExpression) obj).getExpressionString());
+        } else {
+            return false;
+        }
+
+    }
+
+
+    /**
+     * <p>Return the original String used to create this expression,
+     * unmodified.</p>
+     */
+    public String getExpressionString() {
+
+        return this.expression;
+
+    }
+
+
+    /**
+     * <p>Return the hash code for this expression.</p>
+     */
+    public int hashCode() {
+
+        return this.expression.hashCode();
+
+    }
+
+
+    /**
+     * <p>Return <code>true</code> if the expression string for this expression
+     * contains only literal text.</p>
+     */
+    public boolean isLiteralText() {
+
+        return (expression.indexOf("${") < 0) && (expression.indexOf("#{") < 0);
+
+    }
+
+
+    // ------------------------------------------------ MethodExpression Methods
+
+
+    /**
+     * <p>Evaluate the expression relative to the specified context,
+     * and return information about the actual implementation method.</p>
+     *
+     * @param context ELContext for this evaluation
+     */
+    public MethodInfo getMethodInfo(ELContext context) {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+        return new MethodInfo(elements[elements.length - 1], expectedType, signature);
+
+    }
+
+
+    /**
+     * <p>Evaluate the expression relative to the specified ocntext,
+     * and return the result after coercion to the expected result type.</p>
+     *
+     * @param context ELContext for this evaluation
+     * @param params Parameters for this method call
+     */
+    public Object invoke(ELContext context, Object[] params) {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+        if (isLiteralText()) {
+            return expression;
+        }
+
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ELResolver resolver = fcontext.getApplication().getELResolver();
+        Object base = null;
+        for (int i = 0; i < elements.length - 1; i++) {
+            base = resolver.getValue(context, base, elements[i]);
+        }
+
+        try {
+            Method method = base.getClass().getMethod(elements[elements.length - 1], signature);
+            Object result = method.invoke(base, params);
+            return fcontext.getApplication().getExpressionFactory().coerceToType(result, expectedType);
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ELException(e);
+        }
+
+    }
+
+
+    // --------------------------------------------------------- Private Methods
+
+
+    /**
+     * <p>Parse the expression string into its constituent elemetns.</p>
+     */
+    private void parse() {
+
+        if (isLiteralText()) {
+            elements = new String[0];
+            return;
+        }
+
+        if (expression.startsWith("${") || expression.startsWith("#{")) {
+            if (expression.endsWith("}")) {
+                String temp = expression.substring(2, expression.length() - 1).replaceAll(" ", "");
+                List names = new ArrayList();
+                while (temp.length() > 0) {
+                    int period= temp.indexOf(".");
+                    if (period >= 0) {
+                        names.add(temp.substring(0, period));
+                        temp = temp.substring(period + 1);
+                    } else {
+                        names.add(temp);
+                        temp = "";
+                    }
+                }
+                elements = (String[]) names.toArray(new String[names.size()]);
+            } else {
+                throw new IllegalArgumentException(expression);
+            }
+        } else {
+            throw new IllegalArgumentException(expression);
+        }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/MockValueExpression.java b/shale-test/src/main/java/org/apache/shale/test/el/MockValueExpression.java
new file mode 100644
index 0000000..641e6f1
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/MockValueExpression.java
@@ -0,0 +1,306 @@
+/*
+ * 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.shale.test.el;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.el.ELContext;
+import javax.el.ELResolver;
+import javax.el.ValueExpression;
+import javax.faces.context.FacesContext;
+
+/**
+ * <p>Mock implementation of <code>ValueExpression</code>.</p>
+ *
+ * <p>This implementation supports a limited subset of overall expression functionality:</p>
+ * <ul>
+ * <li>A literal string that contains no expression delimiters.</li>
+ * <li>An expression that starts with "#{" or "${", and ends with "}".</li>
+ * </ul>
+ */
+public class MockValueExpression extends ValueExpression {
+    
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * Serial version UID.
+     */
+    private static final long serialVersionUID = -8649071428507512623L;
+
+
+    /**
+     * <p>Construct a new expression for the specified expression string.</p>
+     *
+     * @param expression Expression string to be evaluated
+     * @param expectedType Expected type of the result
+     */
+    public MockValueExpression(String expression, Class expectedType) {
+
+        if (expression == null) {
+            throw new NullPointerException("Expression string cannot be null");
+        }
+        this.expression = expression;
+        this.expectedType = expectedType;
+        parse();
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>The parsed elements of this expression.</p>
+     */
+    private String[] elements = null;
+
+
+    /**
+     * <p>The expected result type for <code>getValue()</code> calls.</p>
+     */
+    private Class expectedType = null;
+
+
+    /**
+     * <p>The original expression string used to create this expression.</p>
+     */
+    private String expression = null;
+
+
+    // ------------------------------------------------------ Expression Methods
+
+
+    /**
+     * <p>Return <code>true</code> if this expression is equal to the
+     * specified expression.</p>
+     *
+     * @param obj Object to be compared
+     */
+    public boolean equals(Object obj) {
+
+        if ((obj != null) & (obj instanceof ValueExpression)) {
+            return expression.equals(((ValueExpression) obj).getExpressionString());
+        } else {
+            return false;
+        }
+
+    }
+
+
+    /**
+     * <p>Return the original String used to create this expression,
+     * unmodified.</p>
+     */
+    public String getExpressionString() {
+
+        return this.expression;
+
+    }
+
+
+    /**
+     * <p>Return the hash code for this expression.</p>
+     */
+    public int hashCode() {
+
+        return this.expression.hashCode();
+
+    }
+
+
+    /**
+     * <p>Return <code>true</code> if the expression string for this expression
+     * contains only literal text.</p>
+     */
+    public boolean isLiteralText() {
+
+        return (expression.indexOf("${") < 0) && (expression.indexOf("#{") < 0);
+
+    }
+
+
+    // ------------------------------------------------- ValueExpression Methods
+
+
+    /**
+     * <p>Return the type that the result of this expression will
+     * be coerced to.</p>
+     */
+    public Class getExpectedType() {
+
+        return this.expectedType;
+
+    }
+
+
+    /**
+     * <p>Evaluate this expression relative to the specified context,
+     * and return the most general type that is acceptable for the
+     * value passed in a <code>setValue()</code> call.</p>
+     *
+     * @param context ELContext for this evaluation
+     */
+    public Class getType(ELContext context) {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+        Object value = getValue(context);
+        if (value == null) {
+            return null;
+        } else {
+            return value.getClass();
+        }
+
+    }
+
+
+    /**
+     * <p>Evaluate this expression relative to the specified context,
+     * and return the result.</p>
+     *
+     * @param context ELContext for this evaluation
+     */
+    public Object getValue(ELContext context) {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+        if (isLiteralText()) {
+            return expression;
+        }
+
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ELResolver resolver = fcontext.getApplication().getELResolver();
+        Object base = null;
+        for (int i = 0; i < elements.length; i++) {
+            base = resolver.getValue(context, base, elements[i]);
+        }
+        return fcontext.getApplication().getExpressionFactory().coerceToType(base, getExpectedType());
+
+    }
+
+
+    /**
+     * <p>Evaluate this expression relative to the specified context,
+     * and return <code>true</code> if a call to <code>setValue()</code>
+     * will always fail.</p>
+     *
+     * @param context ELContext for this evaluation
+     */
+    public boolean isReadOnly(ELContext context) {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+        if (isLiteralText()) {
+            return true;
+        }
+
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ELResolver resolver = fcontext.getApplication().getELResolver();
+        Object base = null;
+        for (int i = 0; i < elements.length - 1; i++) {
+            base = resolver.getValue(context, base, elements[i]);
+        }
+        return resolver.isReadOnly(context, base, elements[elements.length - 1]);
+
+    }
+
+
+
+    /**
+     * <p>Evaluate this expression relative to the specified context,
+     * and set the result to the specified value.</p>
+     *
+     * @param context ELContext for this evaluation
+     * @param value Value to which the result should be set
+     */
+    public void setValue(ELContext context, Object value) {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        ELResolver resolver = fcontext.getApplication().getELResolver();
+        Object base = null;
+        for (int i = 0; i < elements.length - 1; i++) {
+            base = resolver.getValue(context, base, elements[i]);
+        }
+        resolver.setValue(context, base, elements[elements.length - 1], value);
+
+    }
+
+
+    // --------------------------------------------------------- Private Methods
+
+
+    /**
+     * <p>Parse the expression string into its constituent elemetns.</p>
+     */
+    private void parse() {
+
+        if (isLiteralText()) {
+            elements = new String[0];
+            return;
+        }
+
+        if (expression.startsWith("${") || expression.startsWith("#{")) {
+            if (expression.endsWith("}")) {               
+                List names = new ArrayList();
+                StringBuffer expr = new StringBuffer(expression.substring(2, expression.length() - 1).replaceAll(" ", ""));
+                boolean isBlockOn = false;
+                for (int i = expr.length() - 1; i > -1; i--) {
+                    if (expr.charAt(i) == ' ') {
+                        expr.deleteCharAt(i);
+                    } else if (expr.charAt(i) == ']') {
+                        expr.deleteCharAt(i);
+                    } else if (expr.charAt(i) == '[') {
+                        expr.deleteCharAt(i);
+                    } else if (expr.charAt(i) == '\'') {
+                        if (!isBlockOn) {
+                            expr.deleteCharAt(i);
+                        } else {
+                            names.add(0, expr.substring(i + 1));
+                            expr.delete(i, expr.length());
+                        }
+                        isBlockOn = !isBlockOn;
+                    } else if (expr.charAt(i) == '.' && !isBlockOn) {
+                        names.add(0, expr.substring(i + 1));
+                        expr.delete(i, expr.length());
+                    }
+                }
+                if (expr.length() > 0) {
+                    names.add(0, expr.toString());
+                }
+
+                elements = (String[]) names.toArray(new String[names.size()]);
+            } else {
+                throw new IllegalArgumentException(expression);
+            }
+        } else {
+            throw new IllegalArgumentException(expression);
+        }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/MockVariableMapper.java b/shale-test/src/main/java/org/apache/shale/test/el/MockVariableMapper.java
new file mode 100644
index 0000000..2f5c15e
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/MockVariableMapper.java
@@ -0,0 +1,79 @@
+/*
+ * 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.shale.test.el;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.el.ValueExpression;
+import javax.el.VariableMapper;
+
+/**
+ * <p>Mock implementation of <code>VariableMapper</code>.</p>
+ *
+ * @since 1.0.4
+ */
+
+public class MockVariableMapper extends VariableMapper {
+    
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /** Creates a new instance of MockVariableMapper */
+    public MockVariableMapper() {
+    }
+    
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>Map of <code>ValueExpression</code>s, keyed by variable name.</p>
+     */
+    private Map expressions = new HashMap();
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    // -------------------------------------------------- FunctionMapper Methods
+
+
+    /** {@inheritDoc} */
+    public ValueExpression resolveVariable(String variable) {
+
+        return (ValueExpression) expressions.get(variable);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ValueExpression setVariable(String variable, ValueExpression expression) {
+
+        ValueExpression original = (ValueExpression) expressions.get(variable);
+        if (expression == null) {
+            expressions.remove(variable);
+        } else {
+            expressions.put(variable, expression);
+        }
+        return original;
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/MockVariableValueExpression.java b/shale-test/src/main/java/org/apache/shale/test/el/MockVariableValueExpression.java
new file mode 100644
index 0000000..dd4e17f
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/MockVariableValueExpression.java
@@ -0,0 +1,208 @@
+/*
+ * 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.shale.test.el;
+
+import javax.el.ELContext;
+import javax.el.PropertyNotWritableException;
+import javax.el.ValueExpression;
+import javax.faces.context.FacesContext;
+
+/**
+ * <p>Mock implementation of <code>ValueExpression</code> that wraps a variable.</p>
+ */
+public class MockVariableValueExpression extends ValueExpression {
+    
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * Serial version UID.
+     */
+    private static final long serialVersionUID = 4475919948345298291L;
+
+
+    /**
+     * <p>Construct a new expression for the specified instance.</p>
+     *
+     * @param instance Variable instance to be wrapped
+     * @param expectedType Expected type of the result
+     */
+    public MockVariableValueExpression(Object instance, Class expectedType) {
+
+        if (instance == null) {
+            throw new NullPointerException("Instance cannot be null");
+        }
+        this.instance = instance;
+        this.expectedType = expectedType;
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>The expected result type for <code>getValue()</code> calls.</p>
+     */
+    private Class expectedType = null;
+
+
+    /**
+     * <p>The variable instance being wrapped by this expression.</p>
+     */
+    private Object instance = null;
+
+
+    // ------------------------------------------------------ Expression Methods
+
+
+    /**
+     * <p>Return <code>true</code> if this expression is equal to the
+     * specified expression.</p>
+     *
+     * @param obj Object to be compared
+     */
+    public boolean equals(Object obj) {
+
+        if ((obj != null) & (obj instanceof ValueExpression)) {
+            return instance.toString().equals(((ValueExpression) obj).getExpressionString());
+        } else {
+            return false;
+        }
+
+    }
+
+
+    /**
+     * <p>Return the original String used to create this expression,
+     * unmodified.</p>
+     */
+    public String getExpressionString() {
+
+        return this.instance.toString();
+
+    }
+
+
+    /**
+     * <p>Return the hash code for this expression.</p>
+     */
+    public int hashCode() {
+
+        return this.instance.toString().hashCode();
+
+    }
+
+
+    /**
+     * <p>Return <code>true</code> if the expression string for this expression
+     * contains only literal text.</p>
+     */
+    public boolean isLiteralText() {
+
+        return true;
+
+    }
+
+
+    // ------------------------------------------------- ValueExpression Methods
+
+
+    /**
+     * <p>Return the type that the result of this expression will
+     * be coerced to.</p>
+     */
+    public Class getExpectedType() {
+
+        return this.expectedType;
+
+    }
+
+
+    /**
+     * <p>Evaluate this expression relative to the specified context,
+     * and return the most general type that is acceptable for the
+     * value passed in a <code>setValue()</code> call.</p>
+     *
+     * @param context ELContext for this evaluation
+     */
+    public Class getType(ELContext context) {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+        return this.instance.getClass();
+
+    }
+
+
+    /**
+     * <p>Evaluate this expression relative to the specified context,
+     * and return the result.</p>
+     *
+     * @param context ELContext for this evaluation
+     */
+    public Object getValue(ELContext context) {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+        FacesContext fcontext = (FacesContext) context.getContext(FacesContext.class);
+        return fcontext.getApplication().getExpressionFactory().coerceToType(instance, expectedType);
+
+    }
+
+
+    /**
+     * <p>Evaluate this expression relative to the specified context,
+     * and return <code>true</code> if a call to <code>setValue()</code>
+     * will always fail.</p>
+     *
+     * @param context ELContext for this evaluation
+     */
+    public boolean isReadOnly(ELContext context) {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+        return true;
+
+    }
+
+
+
+    /**
+     * <p>Evaluate this expression relative to the specified context,
+     * and set the result to the specified value.</p>
+     *
+     * @param context ELContext for this evaluation
+     * @param value Value to which the result should be set
+     */
+    public void setValue(ELContext context, Object value) {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+
+        throw new PropertyNotWritableException();
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/el/package.html b/shale-test/src/main/java/org/apache/shale/test/el/package.html
new file mode 100644
index 0000000..f539ed5
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/el/package.html
@@ -0,0 +1,21 @@
+<body>
+<!--
+ 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.
+-->
+    <p>This package contains mock objects and supporting infrastructure
+    for expression evaluation using the Unified EL APIs.  These classes
+    are only relevant when running in a JSF 1.2 environment.</p>
+</body>
\ No newline at end of file
diff --git a/shale-test/src/main/java/org/apache/shale/test/htmlunit/AbstractHtmlUnitTestCase.java b/shale-test/src/main/java/org/apache/shale/test/htmlunit/AbstractHtmlUnitTestCase.java
new file mode 100644
index 0000000..b4f8435
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/htmlunit/AbstractHtmlUnitTestCase.java
@@ -0,0 +1,330 @@
+/*
+ * 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.shale.test.htmlunit;
+
+import com.gargoylesoftware.htmlunit.ElementNotFoundException;
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
+import com.gargoylesoftware.htmlunit.html.HtmlBody;
+import com.gargoylesoftware.htmlunit.html.HtmlElement;
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlHead;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Iterator;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+
+
+/**
+ * <p>Abstract base class for system integration tests based on HtmlUnit.
+ * These tests will expect a system property named <code>url</code> to be
+ * present, which will define the URL (including the context path, but
+ * without a trailing slash) of the application to be tested.</p>
+ */
+
+public abstract class AbstractHtmlUnitTestCase extends TestCase {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Construct a new instance of this test case.</p>
+     *
+     * @param name Name of the new test case
+     */
+    public AbstractHtmlUnitTestCase(String name) {
+
+        super(name);
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>The most recently retrieved page from the server.</p>
+     */
+    protected HtmlPage page = null;
+
+
+    /**
+     * <p>The calculated URL for the installed "systest" web application.
+     * This value is based on a system property named <code>url</code>,
+     * which must be defined as part of the command line that executes
+     * each test case.</p>
+     */
+    protected URL url = null;
+
+
+    /**
+     * <p>The web client for this test case.</p>
+     */
+    protected WebClient webClient = null;
+
+
+    // ------------------------------------------------------ Test Setup Methods
+
+
+    /**
+     * <p>Set up the instance variables required for this test case.</p>
+     *
+     * @exception Exception if an error occurs
+     */
+    protected void setUp() throws Exception {
+
+        // Calculate the URL for the installed "systest" web application
+        String url = System.getProperty("url");
+        this.url = new URL(url + "/");
+
+        // Initialize HtmlUnit constructs for this test case
+        webClient = new WebClient();
+
+    }
+
+
+    /**
+     * <p>Return the set of tests included in this test suite.</p>
+     */
+    public static Test suite() {
+
+        return (new TestSuite(AbstractHtmlUnitTestCase.class));
+
+    }
+
+
+    /**
+     * <p>Tear down instance variables required by this test case.</p>
+     */
+    protected void tearDown() throws Exception {
+
+        page = null;
+        url = null;
+        webClient = null;
+
+    }
+
+
+
+    // ------------------------------------------------------- Protected Methods
+
+
+    /**
+     * <p>Return the body element for the most recently retrieved page.
+     * If there is no such element, return <code>null</code>.</p>
+     *
+     * @exception Exception if an error occurs
+     */
+    protected HtmlBody body() throws Exception {
+
+        Iterator elements = page.getAllHtmlChildElements();
+        while (elements.hasNext()) {
+            HtmlElement element = (HtmlElement) elements.next();
+            if (element instanceof HtmlBody) {
+                return ((HtmlBody) element);
+            }
+        }
+        return (null);
+
+    }
+
+
+    /**
+     * <p>Return the HTML element with the specified <code>id</code> from the
+     * most recently retrieved page.  If there is no such element, return
+     * <code>null</code>.</p>
+     *
+     * @param id Identifier of the requested element.
+     *
+     * @exception Exception if an error occurs
+     */
+    protected HtmlElement element(String id) throws Exception {
+
+        try {
+            return (page.getHtmlElementById(id));
+        } catch (ElementNotFoundException e) {
+            return (null);
+        }
+
+    }
+
+
+    /**
+     * <p>Return the form with the specified <code>id</code> from the most
+     * recently retrieved page.  If there is no such form, return
+     * <code>null</code>.<p>
+     *
+     * @param id Identifier of the requested form.
+     *
+     * @exception Exception if an error occurs
+     */
+    protected HtmlForm form(String id) throws Exception {
+
+        Iterator forms = page.getForms().iterator();
+        while (forms.hasNext()) {
+            HtmlForm form = (HtmlForm) forms.next();
+            if (id.equals(form.getAttributeValue("id"))) {
+                return (form);
+            }
+        }
+        return (null);
+
+    }
+
+
+    /**
+     * <p>Return the head element for the most recently retrieved page.
+     * If there is no such element, return <code>null</code>.</p>
+     *
+     * @exception Exception if an error occurs
+     */
+    protected HtmlHead head() throws Exception {
+
+        Iterator elements = page.getAllHtmlChildElements();
+        while (elements.hasNext()) {
+            HtmlElement element = (HtmlElement) elements.next();
+            if (element instanceof HtmlHead) {
+                return ((HtmlHead) element);
+            }
+        }
+        return (null);
+
+    }
+
+
+    /**
+     * <p>Click the specified hyperlink, and retrieve the subsequent page,
+     * saving a reference so that other utility methods may be used to
+     * retrieve information from it.</p>
+     *
+     * @param anchor Anchor component to click
+     *
+     * @exception IOException if an input/output error occurs
+     */
+    protected HtmlPage link(HtmlAnchor anchor) throws IOException {
+
+        HtmlPage page = (HtmlPage) anchor.click();
+        this.page = page;
+        return page;
+
+    }
+
+
+    /**
+     * <p>Return the currently stored page reference.</p>
+     */
+    protected HtmlPage page() {
+
+        return this.page;
+
+    }
+
+
+    /**
+     * <p>Retrieve and return the page at the specified context relative path.
+     * Save a reference to this page so that other utility methods may be used
+     * to retrieve information from it.</p>
+     *
+     * @param path Context relative path
+     *
+     * @exception IllegalArgumentException if the context relative path
+     *  does not begin with a '/' character
+     * @exception Exception if a different error occurs
+     */
+    protected HtmlPage page(String path) throws Exception {
+
+        HtmlPage page = (HtmlPage) webClient.getPage(url(path));
+        this.page = page;
+        return (page);
+
+    }
+
+
+    /**
+     * <p>Reset the stored page reference to the specified value.  This is
+     * useful for scenarios testing resubmit of the same page (simulating the
+     * user pressing the back button and then submitting again).</p>
+     *
+     * @param page Previously saved page to which to reset
+     */
+    protected void reset(HtmlPage page) {
+
+        this.page = page;
+
+    }
+
+
+    /**
+     * <p>Submit the current page, using the specified component, and retrieve
+     * the subsequent page, saving a reference so that other utility methods
+     * may be used to retrieve information from it.</p>
+     *
+     * @param submit Submit button component to click
+     *
+     * @exception IOException if an input/output error occurs
+     */
+    protected HtmlPage submit(HtmlSubmitInput submit) throws IOException {
+
+        HtmlPage page = (HtmlPage) submit.click();
+        this.page = page;
+        return page;
+
+    }
+
+
+    /**
+     * <p>Return the page title from the most recently retrieved page.
+     * Any leading and trailing whitespace will be trimmed.</p>
+     *
+     * @exception Exception if an error occurs
+     */
+    protected String title() throws Exception {
+
+        return (page.getTitleText().trim());
+
+    }
+
+
+    /**
+     * <p>Calculate and return an absolute URL for the specified context
+     * relative path, which must begin with a '/' character.</p>
+     *
+     * @param path Context relative path
+     *
+     * @exception IllegalArgumentException if the context relative path
+     *  does not begin with a '/' character
+     * @exception Exception if a different error ocurs
+     */
+    protected URL url(String path) throws Exception {
+
+        if (path.charAt(0) != '/') {
+            throw new IllegalArgumentException("Context path '" + path
+                                               + "' does not start with '/'");
+        }
+        return new URL(url, path.substring(1));
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/jmock/AbstractJmockJsfTestCase.java b/shale-test/src/main/java/org/apache/shale/test/jmock/AbstractJmockJsfTestCase.java
new file mode 100644
index 0000000..0e91c31
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/jmock/AbstractJmockJsfTestCase.java
@@ -0,0 +1,188 @@
+/*
+ * 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.shale.test.jmock;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+
+import javax.faces.FactoryFinder;
+import javax.faces.application.ApplicationFactory;
+import javax.faces.component.UIViewRoot;
+import javax.faces.lifecycle.LifecycleFactory;
+import javax.faces.render.RenderKitFactory;
+
+import org.apache.shale.test.mock.MockApplication;
+import org.apache.shale.test.mock.MockExternalContext;
+import org.apache.shale.test.mock.MockFacesContext;
+import org.apache.shale.test.mock.MockFacesContextFactory;
+import org.apache.shale.test.mock.MockHttpServletRequest;
+import org.apache.shale.test.mock.MockHttpServletResponse;
+import org.apache.shale.test.mock.MockHttpSession;
+import org.apache.shale.test.mock.MockRenderKit;
+import org.apache.shale.test.mock.MockServletConfig;
+import org.apache.shale.test.mock.MockServletContext;
+import org.apache.shale.test.mock.lifecycle.MockLifecycle;
+import org.apache.shale.test.mock.lifecycle.MockLifecycleFactory;
+import org.jmock.cglib.MockObjectTestCase;
+
+/**
+ * <p>Abstract JMock test case base class, which sets up the JavaServer Faces
+ * mock object environment for a particular simulated request.  The following
+ * protected variables are initialized in the <code>setUp()</code> method, and
+ * cleaned up in the <code>tearDown()</code> method:</p>
+ * <ul>
+ * <li><code>application</code> (<code>MockApplication</code>)</li>
+ * <li><code>config</code> (<code>MockServletConfig</code>)</li>
+ * <li><code>externalContext</code> (<code>MockExternalContext</code>)</li>
+ * <li><code>facesContext</code> (<code>MockFacesContext</code>)</li>
+ * <li><code>lifecycle</code> (<code>MockLifecycle</code>)</li>
+ * <li><code>request</code> (<code>MockHttpServletRequest</code></li>
+ * <li><code>response</code> (<code>MockHttpServletResponse</code>)</li>
+ * <li><code>servletContext</code> (<code>MockServletContext</code>)</li>
+ * <li><code>session</code> (<code>MockHttpSession</code>)</li>
+ * </ul>
+ *
+ * <p>In addition, appropriate factory classes will have been registered with
+ * <code>javax.faces.FactoryFinder</code> for <code>Application</code> and
+ * <code>RenderKit</code> instances.  The created <code>FacesContext</code>
+ * instance will also have been registered in the apppriate thread local
+ * variable, to simulate what a servlet container would do.</p>
+ *
+ * <p><strong>WARNING</strong> - If you choose to subclass this class, be sure
+ * your <code>setUp()</code> and <code>tearDown()</code> methods call
+ * <code>super.setUp()</code> and <code>super.tearDown()</code> respectively,
+ * and that you implement your own <code>suite()</code> method that exposes
+ * the test methods for your test case.</p>
+ */
+
+public abstract class AbstractJmockJsfTestCase extends MockObjectTestCase {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    // Construct a new instance of this test case.
+    public AbstractJmockJsfTestCase(String name) {
+        setName(name);
+    }
+
+
+    // ---------------------------------------------------- Overall Test Methods
+
+
+    // Set up instance variables required by this test case.
+    protected void setUp() throws Exception {
+        
+        // Set up a new thread context class loader
+        threadContextClassLoader = Thread.currentThread().getContextClassLoader();
+        Thread.currentThread().setContextClassLoader(new URLClassLoader(new URL[0],
+                this.getClass().getClassLoader()));
+        
+        // Set up Servlet API Objects
+        servletContext = new MockServletContext();
+        config = new MockServletConfig(servletContext);
+        session = new MockHttpSession();
+        session.setServletContext(servletContext);
+        request = new MockHttpServletRequest(session);
+        request.setServletContext(servletContext);
+        response = new MockHttpServletResponse();
+        
+        // Set up JSF API Objects
+        FactoryFinder.releaseFactories();
+        FactoryFinder.setFactory(FactoryFinder.APPLICATION_FACTORY,
+        "org.apache.shale.test.mock.MockApplicationFactory");
+        FactoryFinder.setFactory(FactoryFinder.FACES_CONTEXT_FACTORY,
+        "org.apache.shale.test.mock.MockFacesContextFactory");
+        FactoryFinder.setFactory(FactoryFinder.LIFECYCLE_FACTORY,
+        "org.apache.shale.test.mock.lifecycle.MockLifecycleFactory");
+        FactoryFinder.setFactory(FactoryFinder.RENDER_KIT_FACTORY,
+        "org.apache.shale.test.mock.MockRenderKitFactory");
+        
+        externalContext =
+            new MockExternalContext(servletContext, request, response);
+        lifecycleFactory = (MockLifecycleFactory)
+        FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
+        lifecycle = (MockLifecycle)
+        lifecycleFactory.getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE);
+        facesContextFactory = (MockFacesContextFactory)
+        FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
+        facesContext = (MockFacesContext)
+        facesContextFactory.getFacesContext(servletContext,
+                request,
+                response,
+                lifecycle);
+        externalContext = (MockExternalContext) facesContext.getExternalContext();
+        UIViewRoot root = new UIViewRoot();
+        root.setViewId("/viewId");
+        root.setRenderKitId(RenderKitFactory.HTML_BASIC_RENDER_KIT);
+        facesContext.setViewRoot(root);
+        ApplicationFactory applicationFactory = (ApplicationFactory)
+          FactoryFinder.getFactory(FactoryFinder.APPLICATION_FACTORY);
+        application = (MockApplication) applicationFactory.getApplication();
+        facesContext.setApplication(application);
+        RenderKitFactory renderKitFactory = (RenderKitFactory)
+        FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
+        renderKit = new MockRenderKit();
+        renderKitFactory.addRenderKit(RenderKitFactory.HTML_BASIC_RENDER_KIT, renderKit);
+        
+    }
+
+    // Tear down instance variables required by this test case.
+    protected void tearDown() throws Exception {
+
+        application = null;
+        config = null;
+        externalContext = null;
+        facesContext.release();
+        facesContext = null;
+        lifecycle = null;
+        lifecycleFactory = null;
+        renderKit = null;
+        request = null;
+        response = null;
+        servletContext = null;
+        session = null;
+        FactoryFinder.releaseFactories();
+
+        Thread.currentThread().setContextClassLoader(threadContextClassLoader);
+        threadContextClassLoader = null;
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    // Mock object instances for our tests
+    protected MockApplication         application = null;
+    protected MockServletConfig       config = null;
+    protected MockExternalContext     externalContext = null;
+    protected MockFacesContext        facesContext = null;
+    protected MockFacesContextFactory facesContextFactory = null;
+    protected MockLifecycle           lifecycle = null;
+    protected MockLifecycleFactory    lifecycleFactory = null;
+    protected MockRenderKit           renderKit = null;
+    protected MockHttpServletRequest  request = null;
+    protected MockHttpServletResponse response = null;
+    protected MockServletContext      servletContext = null;
+    protected MockHttpSession         session = null;
+
+    // Thread context class loader saved and restored after each test
+    private ClassLoader threadContextClassLoader = null;
+
+}
\ No newline at end of file
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockActionListener.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockActionListener.java
new file mode 100644
index 0000000..02e8197
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockActionListener.java
@@ -0,0 +1,65 @@
+/*
+ * 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.shale.test.mock;
+
+import javax.faces.event.AbortProcessingException;
+import javax.faces.event.ActionEvent;
+import javax.faces.event.ActionListener;
+
+/**
+ * <p>Mock implementation of the default <code>ActionListener</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockActionListener implements ActionListener {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Construct a default instance.</p>
+     */
+    public MockActionListener() {
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    // -------------------------------------------------- ActionListener Methods
+
+
+    /**
+     * <p>Process the specified <code>ActionEvent</code>.</p>
+     *
+     * @param event Event to be processed
+     *
+     * @exception AbortProcessingException if further event firing
+     *  should be skipped
+     */
+    public void processAction(ActionEvent event) throws AbortProcessingException {
+        // FIXME - provide default implementation
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockApplication.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockApplication.java
new file mode 100644
index 0000000..b112ab7
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockApplication.java
@@ -0,0 +1,463 @@
+/*
+ * 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.shale.test.mock;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.faces.FacesException;
+import javax.faces.application.Application;
+import javax.faces.application.NavigationHandler;
+import javax.faces.application.StateManager;
+import javax.faces.application.ViewHandler;
+import javax.faces.component.UIComponent;
+import javax.faces.context.FacesContext;
+import javax.faces.convert.Converter;
+import javax.faces.el.MethodBinding;
+import javax.faces.el.PropertyResolver;
+import javax.faces.el.ValueBinding;
+import javax.faces.el.VariableResolver;
+import javax.faces.event.ActionListener;
+import javax.faces.render.RenderKitFactory;
+import javax.faces.validator.Validator;
+
+/**
+ * <p>Mock implementation of <code>Application</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockApplication extends Application {
+
+
+    // ------------------------------------------------------------ Constructors
+
+    /**
+     * <p>Construct a default instance.</p>
+     */
+    public MockApplication() {
+
+        setActionListener(new MockActionListener());
+        components = new HashMap();
+        converters = new HashMap();
+        converters1 = new HashMap();
+        setDefaultLocale(Locale.getDefault());
+        setDefaultRenderKitId(RenderKitFactory.HTML_BASIC_RENDER_KIT);
+        setNavigationHandler(new MockNavigationHandler());
+        setPropertyResolver(new MockPropertyResolver());
+        setStateManager(new MockStateManager());
+        setSupportedLocales(new ArrayList());
+        validators = new HashMap();
+        setVariableResolver(new MockVariableResolver());
+        setViewHandler(new MockViewHandler());
+
+        // Register the standard by-id converters
+        addConverter("javax.faces.BigDecimal", "javax.faces.convert.BigDecimalConverter");
+        addConverter("javax.faces.BigInteger", "javax.faces.convert.BigIntegerConverter");
+        addConverter("javax.faces.Boolean",    "javax.faces.convert.BooleanConverter");
+        addConverter("javax.faces.Byte",       "javax.faces.convert.ByteConverter");
+        addConverter("javax.faces.Character",  "javax.faces.convert.CharacterConverter");
+        addConverter("javax.faces.DateTime",   "javax.faces.convert.DateTimeConverter");
+        addConverter("javax.faces.Double",     "javax.faces.convert.DoubleConverter");
+        addConverter("javax.faces.Float",      "javax.faces.convert.FloatConverter");
+        addConverter("javax.faces.Integer",    "javax.faces.Convert.IntegerConverter");
+        addConverter("javax.faces.Long",       "javax.faces.convert.LongConverter");
+        addConverter("javax.faces.Number",     "javax.faces.convert.NumberConverter");
+        addConverter("javax.faces.Short",      "javax.faces.convert.ShortConverter");
+
+        // Register the standard by-type converters
+        addConverter(Boolean.class,            "javax.faces.convert.BooleanConverter");
+        addConverter(Boolean.TYPE,             "javax.faces.convert.BooleanConverter");
+        addConverter(Byte.class,               "javax.faces.convert.ByteConverter");
+        addConverter(Byte.TYPE,                "javax.faces.convert.ByteConverter");
+        addConverter(Character.class,          "javax.faces.convert.CharacterConverter");
+        addConverter(Character.TYPE,           "javax.faces.convert.CharacterConverter");
+        addConverter(Double.class,             "javax.faces.convert.DoubleConverter");
+        addConverter(Double.TYPE,              "javax.faces.convert.DoubleConverter");
+        addConverter(Float.class,              "javax.faces.convert.FloatConverter");
+        addConverter(Float.TYPE,               "javax.faces.convert.FloatConverter");
+        addConverter(Integer.class,            "javax.faces.convert.IntegerConverter");
+        addConverter(Integer.TYPE,             "javax.faces.convert.IntegerConverter");
+        addConverter(Long.class,               "javax.faces.convert.LongConverter");
+        addConverter(Long.TYPE,                "javax.faces.convert.LongConverter");
+        addConverter(Short.class,              "javax.faces.convert.ShortConverter");
+        addConverter(Short.TYPE,               "javax.faces.convert.ShortConverter");
+
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    private ActionListener actionListener = null;
+    private Map components = null;
+    private Map converters = null; // By id
+    private Map converters1 = null; // By type
+    private Locale defaultLocale = null;
+    private String defaultRenderKitId = null;
+    private String messageBundle = null;
+    private NavigationHandler navigationHandler = null;
+    private PropertyResolver propertyResolver = null;
+    private StateManager stateManager = null;
+    private Collection supportedLocales = null;
+    private Map validators = null;
+    private VariableResolver variableResolver = null;
+    private ViewHandler viewHandler = null;
+
+
+    // ----------------------------------------------------- Application Methods
+
+
+    /** {@inheritDoc} */
+    public ActionListener getActionListener() {
+
+        return this.actionListener;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setActionListener(ActionListener actionListener) {
+        this.actionListener = actionListener;
+    }
+
+
+    /** {@inheritDoc} */
+    public Locale getDefaultLocale() {
+
+        return this.defaultLocale;
+
+    }
+
+    /** {@inheritDoc} */
+    public void setDefaultLocale(Locale defaultLocale) {
+
+        this.defaultLocale = defaultLocale;
+
+    }
+
+    /** {@inheritDoc} */
+    public String getDefaultRenderKitId() {
+
+        return this.defaultRenderKitId;
+
+    }
+
+    /** {@inheritDoc} */
+    public void setDefaultRenderKitId(String defaultRenderKitId) {
+
+        this.defaultRenderKitId = defaultRenderKitId;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getMessageBundle() {
+
+        return this.messageBundle;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setMessageBundle(String messageBundle) {
+
+        this.messageBundle = messageBundle;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public NavigationHandler getNavigationHandler() {
+
+        return this.navigationHandler;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setNavigationHandler(NavigationHandler navigationHandler) {
+
+        this.navigationHandler = navigationHandler;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public PropertyResolver getPropertyResolver() {
+
+        return this.propertyResolver;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setPropertyResolver(PropertyResolver propertyResolver) {
+
+        this.propertyResolver = propertyResolver;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public StateManager getStateManager() {
+
+        return this.stateManager;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setStateManager(StateManager stateManager) {
+
+        this.stateManager = stateManager;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Iterator getSupportedLocales() {
+
+        return this.supportedLocales.iterator();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setSupportedLocales(Collection supportedLocales) {
+
+        this.supportedLocales = supportedLocales;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public VariableResolver getVariableResolver() {
+
+        return this.variableResolver;
+    }
+
+
+    /** {@inheritDoc} */
+    public void setVariableResolver(VariableResolver variableResolver) {
+
+        this.variableResolver = variableResolver;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ViewHandler getViewHandler() {
+
+        return this.viewHandler;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setViewHandler(ViewHandler viewHandler) {
+
+        this.viewHandler = viewHandler;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void addComponent(String componentType, String componentClass) {
+
+        components.put(componentType, componentClass);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public UIComponent createComponent(String componentType) {
+
+        if (componentType == null) {
+            throw new NullPointerException("Requested component type is null");
+        }
+        String componentClass = (String) components.get(componentType);
+        if (componentClass == null) {
+            throw new FacesException("No component class registered for component type '"
+                    + componentType + "'");
+        }
+        try {
+            Class clazz = Class.forName(componentClass);
+            return ((UIComponent) clazz.newInstance());
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public UIComponent createComponent(ValueBinding componentBinding,
+                                       FacesContext context,
+                                       String componentType)
+        throws FacesException {
+
+        UIComponent component = null;
+        try {
+            component = (UIComponent) componentBinding.getValue(context);
+            if (component == null) {
+                component = createComponent(componentType);
+                componentBinding.setValue(context, component);
+            }
+
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+        return component;
+    }
+
+
+    /** {@inheritDoc} */
+    public Iterator getComponentTypes() {
+
+        return (components.keySet().iterator());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void addConverter(String converterId, String converterClass) {
+
+        converters.put(converterId, converterClass);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void addConverter(Class targetClass, String converterClass) {
+
+        converters1.put(targetClass, converterClass);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Converter createConverter(String converterId) {
+
+        String converterClass = (String) converters.get(converterId);
+        if (converterClass == null) {
+            return null;
+        }
+        try {
+            Class clazz = Class.forName(converterClass);
+            return ((Converter) clazz.newInstance());
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Converter createConverter(Class targetClass) {
+
+        String converterClass = (String) converters1.get(targetClass);
+        if (converterClass == null) {
+            return null;
+        }
+        try {
+            Class clazz = Class.forName(converterClass);
+            return ((Converter) clazz.newInstance());
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Iterator getConverterIds() {
+
+        return (converters.keySet().iterator());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Iterator getConverterTypes() {
+
+        return (converters1.keySet().iterator());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public MethodBinding createMethodBinding(String ref, Class[] params) {
+
+        if (ref == null) {
+            throw new NullPointerException();
+        } else {
+            return (new MockMethodBinding(this, ref, params));
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ValueBinding createValueBinding(String ref) {
+
+        if (ref == null) {
+            throw new NullPointerException();
+        } else {
+            return (new MockValueBinding(this, ref));
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void addValidator(String validatorId, String validatorClass) {
+
+        validators.put(validatorId, validatorClass);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Validator createValidator(String validatorId) {
+
+        String validatorClass = (String) validators.get(validatorId);
+        try {
+            Class clazz = Class.forName(validatorClass);
+            return ((Validator) clazz.newInstance());
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Iterator getValidatorIds() {
+        return (validators.keySet().iterator());
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockApplication12.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockApplication12.java
new file mode 100644
index 0000000..9099b24
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockApplication12.java
@@ -0,0 +1,274 @@
+/*
+ * 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.shale.test.mock;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+import javax.el.ArrayELResolver;
+import javax.el.BeanELResolver;
+import javax.el.CompositeELResolver;
+import javax.el.ELContextListener;
+import javax.el.ELException;
+import javax.el.ELResolver;
+import javax.el.ExpressionFactory;
+import javax.el.ListELResolver;
+import javax.el.MapELResolver;
+import javax.el.ResourceBundleELResolver;
+import javax.el.ValueExpression;
+import javax.faces.FacesException;
+import javax.faces.component.UIComponent;
+import javax.faces.component.UIViewRoot;
+import javax.faces.context.FacesContext;
+import org.apache.shale.test.el.FacesImplicitObjectELResolver;
+import org.apache.shale.test.el.FacesPropertyResolverChainWrapper;
+import org.apache.shale.test.el.FacesResourceBundleELResolver;
+import org.apache.shale.test.el.FacesScopedAttributeELResolver;
+import org.apache.shale.test.el.FacesVariableResolverChainWrapper;
+import org.apache.shale.test.el.MockExpressionFactory;
+
+/**
+ * <p>Mock implementation of <code>ExternalContext</code> that includes the semantics
+ * added by JavaServer Faces 1.2.</p>
+ *
+ * $Id$
+ *
+ * @since 1.0.4
+ */
+public class MockApplication12 extends MockApplication {
+    
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Construct a default instance.</p>
+     */
+    public MockApplication12() {
+
+        super();
+
+        // Configure our expression factory and EL resolvers
+        expressionFactory = new MockExpressionFactory();
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>A list of resource bundles configured for this application.</p>
+     */
+    private Map bundles = new HashMap();
+
+
+    /**
+     * <p>The set of configured ELContextListener instances.</p>
+     */
+    private List elContextListeners = new ArrayList();
+
+
+    /**
+     * <p>Expression factory for this instance.</p>
+     */
+    private ExpressionFactory expressionFactory = null;
+
+
+    /**
+     * <p>The configured composite resolver to be returned by <code>getELResolver()</code>.
+     * This value is lazily instantiated.</p>
+     */
+    private ELResolver resolver = null;
+
+
+    /**
+     * <p>The set of ELResolver instances configured on this instance.</p>
+     */
+    private List resolvers = new ArrayList();
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    /**
+     * <p>Add the specified resource bundle to those associated with
+     * this application.</p>
+     *
+     * @param name Name under which to add this resource bundle
+     * @param bundle ResourceBundle to add
+     */
+    public void addResourceBundle(String name, ResourceBundle bundle) {
+        bundles.put(name, bundle);
+    }
+
+
+    /**
+     * <p>Return a <code>Map</code> of the resource bundles configured
+     * for this application, keyed by name.</p>
+     */
+    public Map getResourceBundles() {
+        return bundles;
+    }
+
+
+    // ----------------------------------------------------- Application Methods
+
+
+    /** {@inheritDoc} */
+    public void addELContextListener(ELContextListener listener) {
+
+        elContextListeners.add(listener);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void addELResolver(ELResolver resolver) {
+
+        // Simulate the restriction that you cannot add resolvers after
+        // the first request has been processed.
+        if (this.resolver != null) {
+            throw new IllegalStateException("Cannot add resolvers now");
+        }
+
+        resolvers.add(resolver);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public UIComponent createComponent(ValueExpression expression,
+                                       FacesContext context,
+                                       String componentType) {
+
+        UIComponent component = null;
+        try {
+            component = (UIComponent) expression.getValue(context.getELContext());
+            if (component == null) {
+                component = createComponent(componentType);
+                expression.setValue(context.getELContext(), component);
+            }
+
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+        return component;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object evaluateExpressionGet(FacesContext context,
+                                        String expression,
+                                        Class expectedType) throws ELException {
+
+        ValueExpression ve = getExpressionFactory().createValueExpression
+          (context.getELContext(), expression, expectedType);
+        return ve.getValue(context.getELContext());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ELContextListener[] getELContextListeners() {
+
+        return (ELContextListener[])
+          elContextListeners.toArray(new ELContextListener[elContextListeners.size()]);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ELResolver getELResolver() {
+
+        if (resolver == null) {
+
+            // Configure a default ELResolver per Section 5.6.2 of JSF 1.2
+            CompositeELResolver composite = new CompositeELResolver();
+
+            composite.add(new FacesImplicitObjectELResolver());
+
+            CompositeELResolver nested = new CompositeELResolver();
+            // FIXME - nested.add() "ELResolvers from application configuration resources"
+            nested.add(new FacesVariableResolverChainWrapper());
+            nested.add(new FacesPropertyResolverChainWrapper());
+            Iterator items = resolvers.iterator();
+            while (items.hasNext()) {
+                nested.add((ELResolver) items.next());
+            }
+            composite.add(nested);
+
+            // composite.add(new faces.ManagedBeanELResolver()); // FIXME
+            composite.add(new ResourceBundleELResolver());
+            composite.add(new FacesResourceBundleELResolver());
+            composite.add(new MapELResolver());
+            composite.add(new ListELResolver());
+            composite.add(new ArrayELResolver());
+            composite.add(new BeanELResolver());
+            composite.add(new FacesScopedAttributeELResolver());
+
+            // Make the resolver we have configured the application wide one
+            resolver = composite;
+
+        }
+        return resolver;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ExpressionFactory getExpressionFactory() {
+
+        return this.expressionFactory;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ResourceBundle getResourceBundle(FacesContext context, String name) {
+
+        if ((context == null) || (name == null)) {
+            throw new NullPointerException();
+        }
+        Locale locale = null;
+        UIViewRoot viewRoot = context.getViewRoot();
+        if (viewRoot != null) {
+            locale = viewRoot.getLocale();
+        }
+        if (locale == null) {
+            locale = Locale.getDefault();
+        }
+        return ResourceBundle.getBundle(name, locale);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void removeELContextListener(ELContextListener listener) {
+
+        elContextListeners.remove(listener);
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockApplicationFactory.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockApplicationFactory.java
new file mode 100644
index 0000000..e8dba9e
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockApplicationFactory.java
@@ -0,0 +1,103 @@
+/*
+ * 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.shale.test.mock;
+
+import javax.faces.FacesException;
+import javax.faces.application.Application;
+import javax.faces.application.ApplicationFactory;
+
+/**
+ * <p>Mock implementation of <code>ApplicationFactory</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockApplicationFactory extends ApplicationFactory {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Construct a default instance.</p>
+     */
+    public MockApplicationFactory() {
+
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>The <code>Application</code> instance to be returned by
+     * this factory.</p>
+     */
+    private Application application = null;
+
+
+    // --------------------------------------------- AppolicationFactory Methods
+
+
+    /** {@inheritDoc} */
+    public Application getApplication() {
+
+        if (this.application == null) {
+            Class clazz = null;
+            try {
+                clazz = this.getClass().getClassLoader().loadClass
+                  ("org.apache.shale.test.mock.MockApplication12");
+                this.application = (MockApplication) clazz.newInstance();
+            } catch (NoClassDefFoundError e) {
+                clazz = null; // We are not running in a JSF 1.2 environment
+            } catch (ClassNotFoundException e) {
+                clazz = null; // Same as above
+            } catch (RuntimeException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new FacesException(e);
+            }
+            if (clazz == null) {
+                try {
+                    clazz = this.getClass().getClassLoader().loadClass
+                      ("org.apache.shale.test.mock.MockApplication");
+                    this.application = (MockApplication) clazz.newInstance();
+                } catch (RuntimeException e) {
+                    throw e;
+                } catch (Exception e) {
+                    throw new FacesException(e);
+                }
+            }
+        }
+        return this.application;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setApplication(Application application) {
+
+        this.application = application;
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockApplicationMap.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockApplicationMap.java
new file mode 100644
index 0000000..9db19e1
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockApplicationMap.java
@@ -0,0 +1,248 @@
+/*
+ * 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.shale.test.mock;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.ServletContext;
+
+/**
+ * <p>Mock impementation of <code>Map</code> for the application scope
+ * attributes managed by {@link MockExternalContext}.</p>
+ *
+ * $Id$
+ */
+
+class MockApplicationMap implements Map {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Construct a default instance.</p>
+     *
+     * @param context ServletContext whose attributes we are wrapping
+     */
+    public MockApplicationMap(ServletContext context) {
+
+        this.context = context;
+
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>The <code>ServletContext</code> instance we are wrapping.</p>
+     */
+    private ServletContext context = null;
+
+
+    // ------------------------------------------------------------- Map Methods
+
+
+    /** {@inheritDoc} */
+    public void clear() {
+
+        Iterator keys = keySet().iterator();
+        while (keys.hasNext()) {
+            context.removeAttribute((String) keys.next());
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean containsKey(Object key) {
+
+        return context.getAttribute(key(key)) != null;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean containsValue(Object value) {
+
+        if (value == null) {
+            return false;
+        }
+        Enumeration keys = context.getAttributeNames();
+        while (keys.hasMoreElements()) {
+            Object next = context.getAttribute((String) keys.nextElement());
+            if (next == value) {
+                return true;
+            }
+        }
+        return false;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Set entrySet() {
+
+        Set set = new HashSet();
+        Enumeration keys = context.getAttributeNames();
+        while (keys.hasMoreElements()) {
+            set.add(context.getAttribute((String) keys.nextElement()));
+        }
+        return set;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean equals(Object o) {
+
+        return context.equals(o);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object get(Object key) {
+
+        return context.getAttribute(key(key));
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int hashCode() {
+
+        return context.hashCode();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isEmpty() {
+
+        return size() < 1;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Set keySet() {
+
+        Set set = new HashSet();
+        Enumeration keys = context.getAttributeNames();
+        while (keys.hasMoreElements()) {
+            set.add(keys.nextElement());
+        }
+        return set;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object put(Object key, Object value) {
+
+        if (value == null) {
+            return remove(key);
+        }
+        String skey = key(key);
+        Object previous = context.getAttribute(skey);
+        context.setAttribute(skey, value);
+        return previous;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void putAll(Map map) {
+
+        Iterator keys = map.keySet().iterator();
+        while (keys.hasNext()) {
+            String key = (String) keys.next();
+            context.setAttribute(key, map.get(key));
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object remove(Object key) {
+
+        String skey = key(key);
+        Object previous = context.getAttribute(skey);
+        context.removeAttribute(skey);
+        return previous;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int size() {
+
+        int n = 0;
+        Enumeration keys = context.getAttributeNames();
+        while (keys.hasMoreElements()) {
+            keys.nextElement();
+            n++;
+        }
+        return n;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Collection values() {
+
+        List list = new ArrayList();
+        Enumeration keys = context.getAttributeNames();
+        while (keys.hasMoreElements()) {
+            list.add(context.getAttribute((String) keys.nextElement()));
+        }
+        return list;
+
+    }
+
+
+    /**
+     * <p>Return the specified key, converted to a String.</p>
+     *
+     * @param key The key to convert
+     */
+    private String key(Object key) {
+
+        if (key == null) {
+            throw new IllegalArgumentException();
+        } else if (key instanceof String) {
+            return (String) key;
+        } else {
+            return key.toString();
+        }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockEnumeration.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockEnumeration.java
new file mode 100644
index 0000000..19a5571
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockEnumeration.java
@@ -0,0 +1,79 @@
+/*
+ * 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.shale.test.mock;
+
+import java.util.Enumeration;
+import java.util.Iterator;
+
+/**
+ * <p>Mock implementation of an <code>Enumeration</code> wrapper around
+ * an <code>Iterator</code>.</p>
+ *
+ * $Id$
+ */
+
+class MockEnumeration implements Enumeration {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Construct a wrapper instance.</p>
+     *
+     * @param iterator The <code>Iterator</code> to be wrapped
+     */
+    public MockEnumeration(Iterator iterator) {
+
+        this.iterator = iterator;
+
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>The <code>Iterator</code> we are wrapping.</p>
+     */
+    private Iterator iterator;
+
+
+    // ----------------------------------------------------- Enumeration Methods
+
+
+    /** {@inheritDoc} */
+    public boolean hasMoreElements() {
+
+        return iterator.hasNext();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object nextElement() {
+
+        return iterator.next();
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockExternalContext.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockExternalContext.java
new file mode 100644
index 0000000..7ac79f3
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockExternalContext.java
@@ -0,0 +1,476 @@
+/*
+ * 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.shale.test.mock;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.faces.FacesException;
+import javax.faces.context.ExternalContext;
+import javax.servlet.ServletContext;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+/**
+ * <p>Mock implementation of <code>ExternalContext</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockExternalContext extends ExternalContext {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Construct a wrapper instance.</p>
+     *
+     * @param context <code>ServletContext</code> for this application
+     * @param request <code>HttpServetRequest</code> for this request
+     * @param response <code>HttpServletResponse</code> for this request
+     */
+    public MockExternalContext(ServletContext context,
+                               HttpServletRequest request,
+                               HttpServletResponse response) {
+
+        this.context = context;
+        this.request = request;
+        this.response = response;
+        applicationMap = new MockApplicationMap(context);
+        requestMap = new MockRequestMap(request);
+
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    private Map applicationMap = null;
+    private ServletContext context = null;
+    protected HttpServletRequest request = null;
+    private Map requestMap = null;
+    protected HttpServletResponse response = null;
+    private Map sessionMap = null;
+    private Map requestCookieMap = new HashMap();
+    private Map requestHeaderMap = new HashMap();
+    private Map requestParameterMap = new HashMap();
+
+
+    // ------------------------------------------------- setters for the mock object
+
+
+    /**
+     * <p>Add a new cookie for this request.</p>
+     *
+     * @param cookie The new cookie
+     */
+    public void addRequestCookieMap(Cookie cookie) {
+        requestParameterMap.put(cookie.getName(), cookie);
+    }
+
+
+    /**
+     * <p>Set the request cookie map for this request.</p>
+     *
+     * @param map The new request cookie map
+     */
+    public void setRequestCookieMap(Map map) {
+        requestParameterMap = map;
+    }
+
+
+    /**
+     * <p>Add the specified request parameter for this request.</p>
+     *
+     * @param key Parameter name
+     * @param value Parameter value
+     */
+    public void addRequestParameterMap(String key, String value) {
+        requestParameterMap.put(key, value);
+    }
+
+
+    /**
+     * <p>Set the request parameter map for this request.</p>
+     *
+     * @param map The new request parameter map
+     */
+    public void setRequestParameterMap(Map map) {
+        requestParameterMap = map;
+    }
+
+    /**
+     * <p>Add the specified request header for this request.</p>
+     *
+     * @param key Parameter name
+     * @param value Parameter value
+     */
+    public void addRequestHeader(String key, String value) {
+        requestHeaderMap.put(key, value);
+    }
+
+
+    /**
+     * <p>Set the request header map for this request.</p>
+     *
+     * @param map The new request header map
+     */
+    public void setRequestHeaderMap(Map map) {
+        requestHeaderMap = map;
+    }
+
+
+
+
+
+    // ------------------------------------------------- ExternalContext Methods
+
+
+    /** {@inheritDoc} */
+    public void dispatch(String requestURI)
+      throws IOException, FacesException {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String encodeActionURL(String sb) {
+
+        return sb;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String encodeNamespace(String aValue) {
+
+        return aValue;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String encodeResourceURL(String sb) {
+
+        return sb;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getApplicationMap() {
+
+        return this.applicationMap;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getAuthType() {
+
+        return request.getAuthType();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object getContext() {
+
+        return context;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getInitParameter(String name) {
+
+        return context.getInitParameter(name);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getInitParameterMap() {
+
+        Map parameterMap = new HashMap();
+        Enumeration names = context.getInitParameterNames();
+        while (names.hasMoreElements()) {
+            String name = (String) names.nextElement();
+            parameterMap.put(name, context.getInitParameter(name));
+        }
+        return Collections.unmodifiableMap(parameterMap);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRemoteUser() {
+
+        return request.getRemoteUser();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object getRequest() {
+
+        return request;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRequestContextPath() {
+
+        return request.getContextPath();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getRequestCookieMap() {
+
+        return requestCookieMap;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getRequestHeaderMap() {
+
+        return requestHeaderMap;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getRequestHeaderValuesMap() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Locale getRequestLocale() {
+
+        return request.getLocale();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Iterator getRequestLocales() {
+
+        return new LocalesIterator(request.getLocales());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getRequestMap() {
+
+        return requestMap;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getRequestParameterMap() {
+
+        return requestParameterMap;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Iterator getRequestParameterNames() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getRequestParameterValuesMap() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRequestPathInfo() {
+
+        return request.getPathInfo();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRequestServletPath() {
+
+        return request.getServletPath();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public URL getResource(String path) throws MalformedURLException {
+
+        return context.getResource(path);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public InputStream getResourceAsStream(String path) {
+
+        return context.getResourceAsStream(path);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Set getResourcePaths(String path) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object getResponse() {
+
+        return response;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object getSession(boolean create) {
+
+        return request.getSession(create);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getSessionMap() {
+
+        if (sessionMap == null) {
+            HttpSession session = request.getSession(true);
+            sessionMap = new MockSessionMap(session);
+        }
+        return sessionMap;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public java.security.Principal getUserPrincipal() {
+
+        return request.getUserPrincipal();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isUserInRole(String role) {
+
+        return request.isUserInRole(role);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void log(String message) {
+
+        context.log(message);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void log(String message, Throwable throwable) {
+
+        context.log(message, throwable);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void redirect(String requestURI)
+      throws IOException {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /**
+     * <p>Iterator implementation that wraps an enumeration
+     * of Locales for the current request.</p>
+     */
+    private class LocalesIterator implements Iterator {
+
+        /**
+         * <p>Construct an iterator wrapping the specified
+         * enumeration.</p>
+         *
+         * @param locales Locales enumeration to wrap
+         */
+        public LocalesIterator(Enumeration locales) {
+            this.locales = locales;
+        }
+
+        /**
+         * <p>The enumeration to be wrapped.</p>
+         */
+        private Enumeration locales;
+
+        /** {@inheritDoc} */
+        public boolean hasNext() { return locales.hasMoreElements(); }
+
+        /** {@inheritDoc} */
+        public Object next() { return locales.nextElement(); }
+
+        /** {@inheritDoc} */
+        public void remove() { throw new UnsupportedOperationException(); }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockExternalContext12.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockExternalContext12.java
new file mode 100644
index 0000000..86e8b6e
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockExternalContext12.java
@@ -0,0 +1,120 @@
+/*
+ * 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.shale.test.mock;
+
+import java.io.UnsupportedEncodingException;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * <p>Mock implementation of <code>ExternalContext</code> that includes the semantics
+ * added by JavaServer Faces 1.2.</p>
+ *
+ * $Id$
+ *
+ * @since 1.0.4
+ */
+
+public class MockExternalContext12 extends MockExternalContext {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    public MockExternalContext12(ServletContext context,
+                                 HttpServletRequest request,
+                                 HttpServletResponse response) {
+        super(context, request, response);
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    // ------------------------------------------------- ExternalContext Methods
+
+
+    /** {@inheritDoc} */
+    public String getRequestCharacterEncoding() {
+
+        return this.request.getCharacterEncoding();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRequestContentType() {
+
+        return this.request.getContentType();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getResponseCharacterEncoding() {
+
+        return this.response.getCharacterEncoding();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getResponseContentType() {
+
+        return this.response.getContentType();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setRequest(Object request) {
+
+        this.request = (HttpServletRequest) request;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setRequestCharacterEncoding(String encoding) throws UnsupportedEncodingException {
+
+        this.request.setCharacterEncoding(encoding);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setResponse(Object response) {
+
+        this.response = (HttpServletResponse) response;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setResponseCharacterEncoding(String encoding) {
+
+        this.response.setCharacterEncoding(encoding);
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockFacesContext.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockFacesContext.java
new file mode 100644
index 0000000..adc2d53
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockFacesContext.java
@@ -0,0 +1,324 @@
+/*
+ * 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.shale.test.mock;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.faces.FactoryFinder;
+import javax.faces.application.Application;
+import javax.faces.application.FacesMessage;
+import javax.faces.application.FacesMessage.Severity;
+import javax.faces.component.UIViewRoot;
+import javax.faces.context.ExternalContext;
+import javax.faces.context.FacesContext;
+import javax.faces.context.ResponseStream;
+import javax.faces.context.ResponseWriter;
+import javax.faces.lifecycle.Lifecycle;
+import javax.faces.render.RenderKit;
+import javax.faces.render.RenderKitFactory;
+
+
+/**
+ * <p>Mock implementation of <code>FacesContext</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockFacesContext extends FacesContext {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    public MockFacesContext() {
+        super();
+        setCurrentInstance(this);
+    }
+
+
+    public MockFacesContext(ExternalContext externalContext) {
+        setExternalContext(externalContext);
+        setCurrentInstance(this);
+    }
+
+
+    public MockFacesContext(ExternalContext externalContext, Lifecycle lifecycle) {
+        this(externalContext);
+        this.lifecycle = lifecycle;
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    /**
+     * <p>Set the <code>Application</code> instance for this instance.</p>
+     *
+     * @param application The new Application
+     */
+    public void setApplication(Application application) {
+
+        this.application = application;
+
+    }
+
+
+    /**
+     * <p>Set the <code>ExternalContext</code> instance for this instance.</p>
+     *
+     * @param externalContext The new ExternalContext
+     */
+    public void setExternalContext(ExternalContext externalContext) {
+
+        this.externalContext = externalContext;
+
+    }
+
+
+    /**
+     * <p>Set the <code>FacesContext</code> instance for this instance.</p>
+     *
+     * @param facesContext The new FacesContext
+     */
+    public static void setCurrentInstance(FacesContext facesContext) {
+
+        FacesContext.setCurrentInstance(facesContext);
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    private Application application = null;
+    private ExternalContext externalContext = null;
+    private Lifecycle lifecycle = null;
+    private Map messages = new HashMap();
+    private boolean renderResponse = false;
+    private boolean responseComplete = false;
+    private ResponseStream responseStream = null;
+    private ResponseWriter responseWriter = null;
+    private UIViewRoot viewRoot = null;
+
+
+    // ---------------------------------------------------- FacesContext Methods
+
+
+    /** {@inheritDoc} */
+    public Application getApplication() {
+
+        return this.application;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Iterator getClientIdsWithMessages() {
+
+        return messages.keySet().iterator();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ExternalContext getExternalContext() {
+
+        return this.externalContext;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Severity getMaximumSeverity() {
+
+        Severity severity = null;
+        Iterator messages = getMessages();
+        while (messages.hasNext()) {
+            FacesMessage message = (FacesMessage) messages.next();
+            if (severity == null) {
+                severity = message.getSeverity();
+            } else if (message.getSeverity().getOrdinal() > severity.getOrdinal()) {
+                severity = message.getSeverity();
+            }
+        }
+        return severity;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Iterator getMessages() {
+
+        ArrayList results = new ArrayList();
+        Iterator clientIds = messages.keySet().iterator();
+        while (clientIds.hasNext()) {
+            String clientId = (String) clientIds.next();
+            results.addAll((List) messages.get(clientId));
+        }
+        return results.iterator();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Iterator getMessages(String clientId) {
+
+        List list = (List) messages.get(clientId);
+        if (list == null) {
+            list = new ArrayList();
+        }
+        return list.iterator();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public RenderKit getRenderKit() {
+
+        UIViewRoot vr = getViewRoot();
+        if (vr == null) {
+            return null;
+        }
+        String renderKitId = vr.getRenderKitId();
+        if (renderKitId == null) {
+            return null;
+        }
+        RenderKitFactory rkFactory = (RenderKitFactory)
+            FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
+        return rkFactory.getRenderKit(this, renderKitId);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean getRenderResponse() {
+
+        return this.renderResponse;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean getResponseComplete() {
+
+        return this.responseComplete;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ResponseStream getResponseStream() {
+
+        return this.responseStream;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setResponseStream(ResponseStream responseStream) {
+
+        this.responseStream = responseStream;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ResponseWriter getResponseWriter() {
+
+        return this.responseWriter;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setResponseWriter(ResponseWriter responseWriter) {
+
+        this.responseWriter = responseWriter;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public UIViewRoot getViewRoot() {
+
+        return this.viewRoot;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setViewRoot(UIViewRoot viewRoot) {
+
+        this.viewRoot = viewRoot;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void addMessage(String clientId, FacesMessage message) {
+
+        if (message == null) {
+            throw new NullPointerException();
+        }
+        List list = (List) messages.get(clientId);
+        if (list == null) {
+            list = new ArrayList();
+            messages.put(clientId, list);
+        }
+        list.add(message);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void release() {
+
+        application = null;
+        externalContext = null;
+        messages.clear();
+        renderResponse = false;
+        responseComplete = false;
+        responseStream = null;
+        responseWriter = null;
+        viewRoot = null;
+        setCurrentInstance(null);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void renderResponse() {
+
+        this.renderResponse = true;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void responseComplete() {
+
+        this.responseComplete = true;
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockFacesContext12.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockFacesContext12.java
new file mode 100644
index 0000000..e5ed652
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockFacesContext12.java
@@ -0,0 +1,114 @@
+/*
+ * 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.shale.test.mock;
+
+import javax.el.ELContext;
+import javax.el.ELContextEvent;
+import javax.el.ELContextListener;
+import javax.faces.context.ExternalContext;
+import javax.faces.context.FacesContext;
+import javax.faces.lifecycle.Lifecycle;
+import org.apache.shale.test.el.MockELContext;
+
+/**
+ * <p>Mock implementation of <code>FacesContext</code> that includes the semantics
+ * added by JavaServer Faces 1.2.</p>
+ *
+ * $Id$
+ *
+ * @since 1.0.4
+ */
+
+public class MockFacesContext12 extends MockFacesContext {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    public MockFacesContext12() {
+        super();
+        setCurrentInstance(this);
+    }
+
+
+    public MockFacesContext12(ExternalContext externalContext) {
+        super(externalContext);
+    }
+
+
+    public MockFacesContext12(ExternalContext externalContext, Lifecycle lifecycle) {
+        super(externalContext, lifecycle);
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    /**
+     * <p>Set the <code>ELContext</code> instance for this instance.</p>
+     *
+     * @param elContext The new ELContext
+     */
+    public void setELContext(ELContext elContext) {
+
+        this.elContext = elContext;
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    private ELContext elContext = null;
+
+
+    // ---------------------------------------------------- FacesContext Methods
+
+
+    /** {@inheritDoc} */
+    public ELContext getELContext() {
+
+        if (this.elContext == null) {
+
+            // Initialize a new ELContext
+            this.elContext = new MockELContext();
+            this.elContext.putContext(FacesContext.class, this);
+
+            // Notify interested listeners that this ELContext was created
+            ELContextListener[] listeners = getApplication().getELContextListeners();
+            if ((listeners != null) && (listeners.length > 0)) {
+                ELContextEvent event = new ELContextEvent(this.elContext);
+                for (int i = 0; i < listeners.length; i++) {
+                    listeners[i].contextCreated(event);
+                }
+            }
+
+        }
+        return this.elContext;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void release() {
+        super.release();
+        this.elContext = null;
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockFacesContextFactory.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockFacesContextFactory.java
new file mode 100644
index 0000000..9b6aee9
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockFacesContextFactory.java
@@ -0,0 +1,175 @@
+/*
+ * 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.shale.test.mock;
+
+import java.lang.reflect.Constructor;
+import javax.faces.FacesException;
+import javax.faces.context.ExternalContext;
+import javax.faces.context.FacesContext;
+import javax.faces.context.FacesContextFactory;
+import javax.faces.lifecycle.Lifecycle;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * <p>Mock implementation of <code>FacesContextFactory</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockFacesContextFactory extends FacesContextFactory {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Look up the constructor we will use for creating <code>MockFacesContext</code>
+     * instances.</p>
+     */
+    public MockFacesContextFactory() {
+
+        Class clazz = null;
+
+        // Try to load the 1.2 version of our mock FacesContext class
+        try {
+            clazz = this.getClass().getClassLoader().loadClass("org.apache.shale.test.mock.MockFacesContext12");
+            constructor = clazz.getConstructor(facesContextSignature);
+            jsf12 = true;
+        } catch (NoClassDefFoundError e) {
+            // We are not running on JSF 1.2, so go to our fallback
+            clazz = null;
+            constructor = null;
+        } catch (ClassNotFoundException e) {
+            // Same as above
+            clazz = null;
+            constructor = null;
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+
+        // Fall back to the 1.1 version if we could not load the 1.2 version
+        try {
+            if (clazz == null) {
+                clazz = this.getClass().getClassLoader().loadClass("org.apache.shale.test.mock.MockFacesContext");
+                constructor = clazz.getConstructor(facesContextSignature);
+                jsf12 = false;
+            }
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>The constructor for creating a <code>FacesContext</code> instance,
+     * taking an <code>ExternalContext</code> and <code>Lifecycle</code>.</p>
+     */
+    private Constructor constructor = null;
+
+
+    /**
+     * <p>The parameter signature of the ExternalContext constructor we wish to call.</p>
+     */
+    private static Class[] externalContextSignature = new Class[] {
+        ServletContext.class, HttpServletRequest.class, HttpServletResponse.class
+    };
+
+
+    /**
+     * <p>The parameter signature of the FacesContext constructor we wish to call.</p>
+     */
+    private static Class[] facesContextSignature = new Class[] {
+        ExternalContext.class, Lifecycle.class
+    };
+
+
+    /**
+     * <p>Flag indicating that we are running in a JSF 1.2 environment.</p>
+     */
+    private boolean jsf12 = false;
+
+
+    // --------------------------------------------- FacesContextFactory Methods
+
+
+    /** {@inheritDoc} */
+    public FacesContext getFacesContext(Object context, Object request,
+                                        Object response,
+                                        Lifecycle lifecycle) throws FacesException {
+
+        // Select the appropriate MockExternalContext implementation class
+        Class clazz = MockExternalContext.class;
+        if (jsf12) {
+            try {
+                clazz = this.getClass().getClassLoader().loadClass
+                  ("org.apache.shale.test.mock.MockExternalContext12");
+            } catch (RuntimeException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new FacesException(e);
+            }
+        }
+
+        // Select the constructor we wish to call
+        Constructor mecConstructor = null;
+        try {
+            mecConstructor = clazz.getConstructor(externalContextSignature);
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+
+        // Construct an appropriate MockExternalContext instance
+        MockExternalContext externalContext = null;
+        try {
+            externalContext = (MockExternalContext) mecConstructor.newInstance
+              (new Object[] { context, request, response });
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+
+        // Construct an appropriate MockFacesContext instance and return it
+        try {
+            return (MockFacesContext)
+              constructor.newInstance(new Object[] { externalContext, lifecycle });
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new FacesException(e);
+        }
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockHttpServletRequest.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockHttpServletRequest.java
new file mode 100644
index 0000000..e8d2bb8
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockHttpServletRequest.java
@@ -0,0 +1,894 @@
+/*
+ * 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.shale.test.mock;
+
+import java.io.BufferedReader;
+import java.security.Principal;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.Vector;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequestAttributeEvent;
+import javax.servlet.ServletRequestAttributeListener;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+
+/**
+ * <p>Mock implementation of <code>HttpServletContext</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockHttpServletRequest implements HttpServletRequest {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    public MockHttpServletRequest() {
+
+        super();
+
+    }
+
+
+    public MockHttpServletRequest(HttpSession session) {
+
+        super();
+        setHttpSession(session);
+
+    }
+
+
+    public MockHttpServletRequest(String contextPath, String servletPath,
+                                  String pathInfo, String queryString) {
+
+        super();
+        setPathElements(contextPath, servletPath, pathInfo, queryString);
+
+    }
+
+
+
+    public MockHttpServletRequest(String contextPath, String servletPath,
+                                  String pathInfo, String queryString,
+                                  HttpSession session) {
+
+        super();
+        setPathElements(contextPath, servletPath, pathInfo, queryString);
+        setHttpSession(session);
+
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    /**
+     * <p>Add a new listener instance that should be notified about
+     * attribute changes.</p>
+     *
+     * @param listener The new listener to register
+     */
+    public void addAttributeListener(ServletRequestAttributeListener listener) {
+        attributeListeners.add(listener);
+    }
+
+
+    /**
+     * <p>Add a date-valued header for this request.</p>
+     *
+     * @param name Header name
+     * @param value Header value
+     */
+    public void addDateHeader(String name, long value) {
+
+        headers.add(name + ": " + formatDate(value));
+
+    }
+
+
+    /**
+     * <p>Add a String-valued header for this request.</p>
+     *
+     * @param name Header name
+     * @param value Header value
+     */
+    public void addHeader(String name, String value) {
+
+        headers.add(name + ": " + value);
+
+    }
+
+
+    /**
+     * <p>Add an integer-valued header for this request.</p>
+     *
+     * @param name Header name
+     * @param value Header value
+     */
+    public void addIntHeader(String name, int value) {
+
+        headers.add(name + ": " + value);
+
+    }
+
+
+    /**
+     * <p>Add a request parameter for this request.</p>
+     *
+     * @param name Parameter name
+     * @param value Parameter value
+     */
+    public void addParameter(String name, String value) {
+
+        String[] values = (String[]) parameters.get(name);
+        if (values == null) {
+            String[] results = new String[] { value };
+            parameters.put(name, results);
+            return;
+        }
+        String[] results = new String[values.length + 1];
+        System.arraycopy(values, 0, results, 0, values.length);
+        results[values.length] = value;
+        parameters.put(name, results);
+
+    }
+
+
+    /**
+     * <p>Return the <code>ServletContext</code> associated with
+     * this request.</p>
+     */
+    public ServletContext getServletContext() {
+
+        return this.servletContext;
+
+    }
+
+
+    /**
+     * <p>Set the <code>HttpSession</code> associated with this request.</p>
+     *
+     * @param session The new session
+     */
+    public void setHttpSession(HttpSession session) {
+
+        this.session = session;
+
+    }
+
+
+    /**
+     * <p>Set the <code>Locale</code> associated with this request.</p>
+     *
+     * @param locale The new locale
+     */
+    public void setLocale(Locale locale) {
+
+        this.locale = locale;
+
+    }
+
+
+    /**
+     * <p>Set the parsed path elements associated with this request.</p>
+     *
+     * @param contextPath The context path
+     * @param servletPath The servlet path
+     * @param pathInfo The extra path information
+     * @param queryString The query string
+     */
+    public void setPathElements(String contextPath, String servletPath,
+                                String pathInfo, String queryString) {
+
+        this.contextPath = contextPath;
+        this.servletPath = servletPath;
+        this.pathInfo = pathInfo;
+        this.queryString = queryString;
+
+    }
+
+
+    /**
+     * <p>Set the <code>ServletContext</code> associated with this request.</p>
+     *
+     * @param servletContext The new servlet context
+     */
+    public void setServletContext(ServletContext servletContext) {
+
+        this.servletContext = servletContext;
+
+    }
+
+
+    /**
+     * <p>Set the <code>Principal</code> associated with this request.</p>
+     *
+     * @param principal The new Principal
+     */
+    public void setUserPrincipal(Principal principal) {
+
+        this.principal = principal;
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    private List attributeListeners = new ArrayList();
+    private HashMap attributes = new HashMap();
+    private String contextPath = null;
+    private List headers = new ArrayList();
+    private Locale locale = null;
+    private HashMap parameters = new HashMap();
+    private String pathInfo = null;
+    private Principal principal = null;
+    private String queryString = null;
+    private ServletContext servletContext = null;
+    private String servletPath = null;
+    private HttpSession session = null;
+    private String characterEncoding = null;
+
+
+    // ---------------------------------------------- HttpServletRequest Methods
+
+
+    /** {@inheritDoc} */
+    public String getAuthType() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getContextPath() {
+
+        return contextPath;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Cookie[] getCookies() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public long getDateHeader(String name) {
+
+        String match = name + ":";
+        Iterator headers = this.headers.iterator();
+        while (headers.hasNext()) {
+            String header = (String) headers.next();
+            if (header.startsWith(match)) {
+                return parseDate(header.substring(match.length() + 1).trim());
+            }
+        }
+        return (long) -1;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getHeader(String name) {
+
+        String match = name + ":";
+        Iterator headers = this.headers.iterator();
+        while (headers.hasNext()) {
+            String header = (String) headers.next();
+            if (header.startsWith(match)) {
+                return header.substring(match.length() + 1).trim();
+            }
+        }
+        return null;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getHeaderNames() {
+
+        Vector values = new Vector();
+        Iterator headers = this.headers.iterator();
+        while (headers.hasNext()) {
+            String header = (String) headers.next();
+            int colon = header.indexOf(':');
+            if (colon >= 0) {
+                String name = header.substring(0, colon).trim();
+                if (!values.contains(name)) {
+                    values.add(name);
+                }
+            }
+        }
+        return values.elements();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getHeaders(String name) {
+
+        String match = name + ":";
+        Vector values = new Vector();
+        Iterator headers = this.headers.iterator();
+        while (headers.hasNext()) {
+            String header = (String) headers.next();
+            if (header.startsWith(match)) {
+                values.add(header.substring(match.length() + 1).trim());
+            }
+        }
+        return values.elements();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int getIntHeader(String name) {
+
+        String match = name + ":";
+        Iterator headers = this.headers.iterator();
+        while (headers.hasNext()) {
+            String header = (String) headers.next();
+            if (header.startsWith(match)) {
+                return Integer.parseInt(header.substring(match.length() + 1).trim());
+            }
+        }
+        return -1;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getMethod() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getPathInfo() {
+
+        return pathInfo;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getPathTranslated() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getQueryString() {
+
+        return queryString;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRemoteUser() {
+
+        if (principal != null) {
+            return principal.getName();
+        } else {
+            return null;
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRequestedSessionId() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRequestURI() {
+
+        StringBuffer sb = new StringBuffer();
+        if (contextPath != null) {
+            sb.append(contextPath);
+        }
+        if (servletPath != null) {
+            sb.append(servletPath);
+        }
+        if (pathInfo != null) {
+            sb.append(pathInfo);
+        }
+        if (sb.length() > 0) {
+            return sb.toString();
+        }
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public StringBuffer getRequestURL() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getServletPath() {
+
+        return (servletPath);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public HttpSession getSession() {
+
+        return getSession(true);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public HttpSession getSession(boolean create) {
+
+        if (create && (session == null)) {
+            throw new UnsupportedOperationException();
+        }
+        return session;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Principal getUserPrincipal() {
+
+        return principal;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isRequestedSessionIdFromCookie() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isRequestedSessionIdFromUrl() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isRequestedSessionIdFromURL() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isRequestedSessionIdValid() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isUserInRole(String role) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    // ------------------------------------------------- ServletRequest Methods
+
+
+    /** {@inheritDoc} */
+    public Object getAttribute(String name) {
+
+        return attributes.get(name);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getAttributeNames() {
+
+        return new MockEnumeration(attributes.keySet().iterator());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getCharacterEncoding() {
+
+        return characterEncoding;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int getContentLength() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getContentType() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ServletInputStream getInputStream() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Locale getLocale() {
+
+        return locale;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getLocales() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getLocalAddr() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getLocalName() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int getLocalPort() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getParameter(String name) {
+
+        String[] values = (String[]) parameters.get(name);
+        if (values != null) {
+            return values[0];
+        } else {
+            return null;
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getParameterMap() {
+
+        return parameters;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getParameterNames() {
+
+        return new MockEnumeration(parameters.keySet().iterator());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String[] getParameterValues(String name) {
+
+        return (String[]) parameters.get(name);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getProtocol() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public BufferedReader getReader() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRealPath(String path) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRemoteAddr() {
+
+        // i figure testing never assumes a specific remote - so anything works
+        return "1.2.3.4";
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRemoteHost() {
+
+        // i figure testing never assumes a specific remote - so anything works
+        return "ShaleServer";
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int getRemotePort() {
+
+        // i figure testing never assumes a specific remote - so anything works
+        return 46123;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public RequestDispatcher getRequestDispatcher(String path) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getScheme() {
+
+        return ("http");
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getServerName() {
+
+        return ("localhost");
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int getServerPort() {
+
+        return (8080);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isSecure() {
+
+        return false;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void removeAttribute(String name) {
+
+        if (attributes.containsKey(name)) {
+            Object value = attributes.remove(name);
+            fireAttributeRemoved(name, value);
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setAttribute(String name, Object value) {
+
+        if (name == null) {
+            throw new IllegalArgumentException("Attribute name cannot be null");
+        }
+        if (value == null) {
+            removeAttribute(name);
+            return;
+        }
+        if (attributes.containsKey(name)) {
+            Object oldValue = attributes.get(name);
+            attributes.put(name, value);
+            fireAttributeReplaced(name, oldValue);
+        } else {
+            attributes.put(name, value);
+            fireAttributeAdded(name, value);
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setCharacterEncoding(String characterEncoding) {
+
+        this.characterEncoding = characterEncoding;
+
+    }
+
+
+    // --------------------------------------------------------- Private Methods
+
+
+    /**
+     * <p>Fire an attribute added event to interested listeners.</p>
+     *
+     * @param key Attribute key whose value was added
+     * @param value The new attribute value
+     */
+    private void fireAttributeAdded(String key, Object value) {
+        if (attributeListeners.size() < 1) {
+            return;
+        }
+        ServletRequestAttributeEvent event =
+                new ServletRequestAttributeEvent(getServletContext(), this, key, value);
+        Iterator listeners = attributeListeners.iterator();
+        while (listeners.hasNext()) {
+            ServletRequestAttributeListener listener =
+                    (ServletRequestAttributeListener) listeners.next();
+            listener.attributeAdded(event);
+        }
+    }
+
+
+    /**
+     * <p>Fire an attribute removed event to interested listeners.</p>
+     *
+     * @param key Attribute key whose value was removed
+     * @param value Attribute value that was removed
+     */
+    private void fireAttributeRemoved(String key, Object value) {
+        if (attributeListeners.size() < 1) {
+            return;
+        }
+        ServletRequestAttributeEvent event =
+                new ServletRequestAttributeEvent(getServletContext(), this, key, value);
+        Iterator listeners = attributeListeners.iterator();
+        while (listeners.hasNext()) {
+            ServletRequestAttributeListener listener =
+                    (ServletRequestAttributeListener) listeners.next();
+            listener.attributeRemoved(event);
+        }
+    }
+
+
+    /**
+     * <p>Fire an attribute replaced event to interested listeners.</p>
+     *
+     * @param key Attribute key whose value was replaced
+     * @param value The original value
+     */
+    private void fireAttributeReplaced(String key, Object value) {
+        if (attributeListeners.size() < 1) {
+            return;
+        }
+        ServletRequestAttributeEvent event =
+                new ServletRequestAttributeEvent(getServletContext(), this, key, value);
+        Iterator listeners = attributeListeners.iterator();
+        while (listeners.hasNext()) {
+            ServletRequestAttributeListener listener =
+                    (ServletRequestAttributeListener) listeners.next();
+            listener.attributeReplaced(event);
+        }
+    }
+
+
+    /**
+     * <p>The date formatting helper we will use in <code>httpTimestamp()</code>.
+     * Note that usage of this helper must be synchronized.</p>
+     */
+    private static SimpleDateFormat format =
+            new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
+    static {
+        format.setTimeZone(TimeZone.getTimeZone("GMT"));
+    }
+
+
+    /**
+     * <p>Return a properly formatted String version of the specified
+     * date/time, formatted as required by the HTTP specification.</p>
+     *
+     * @param date Date/time, expressed as milliseconds since the epoch
+     */
+    private String formatDate(long date) {
+        return format.format(new Date(date));
+    }
+
+
+    /**
+     * <p>Return a date/time value, parsed from the specified String.</p>
+     *
+     * @param date Date/time, expressed as a String
+     */
+    private long parseDate(String date) {
+        try {
+            return format.parse(date).getTime();
+        } catch (ParseException e) {
+            throw new IllegalArgumentException(date);
+        }
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockHttpServletResponse.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockHttpServletResponse.java
new file mode 100644
index 0000000..fdbcb5b
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockHttpServletResponse.java
@@ -0,0 +1,454 @@
+/*
+ * 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.shale.test.mock;
+
+import java.io.ByteArrayOutputStream;
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * <p>Mock implementation of <code>HttpServletResponse</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockHttpServletResponse implements HttpServletResponse {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Return a default instance.</p>
+     */
+    public MockHttpServletResponse() { }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    /**
+     * <p>Retrieve the first value that was set for the specified header,
+     * if any.  Otherwise, return <code>null</code>.</p>
+     *
+     * @param name Header name to look up
+     */
+    public String getHeader(String name) {
+        String match = name + ":";
+        Iterator headers = this.headers.iterator();
+        while (headers.hasNext()) {
+            String header = (String) headers.next();
+            if (header.startsWith(match)) {
+                return header.substring(match.length() + 1).trim();
+            }
+        }
+        return null;
+    }
+
+
+    /**
+     * <p>Return the text message for the HTTP status that was set.</p>
+     */
+    public String getMessage() {
+        return this.message;
+    }
+
+
+    /**
+     * <p>Return the HTTP status code that was set.</p>
+     */
+    public int getStatus() {
+        return this.status;
+    }
+
+
+    /**
+     * <p>Set the <code>ServletOutputStream</code> to be returned by a call to
+     * <code>getOutputStream()</code>.</p>
+     *
+     * @param stream The <code>ServletOutputStream</code> instance to use
+     *
+     * @deprecated Let the <code>getOutputStream()</code> method create and
+     *  return an instance of <code>MockServletOutputStream</code> for you
+     */
+    public void setOutputStream(ServletOutputStream stream) {
+        this.stream = stream;
+    }
+
+
+    /**
+     * <p>Set the <code>PrintWriter</code> to be returned by a call to
+     * <code>getWriter()</code>.</p>
+     *
+     * @param writer The <code>PrintWriter</code> instance to use
+     *
+     * @deprecated Let the <code>getWriter()</code> method create and return
+     *  an instance of <code>MockPrintWriter</code> for you
+     */
+    public void setWriter(PrintWriter writer) {
+        this.writer = writer;
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    private String encoding = "ISO-8859-1";
+    private String contentType = "text/html";
+    private List headers = new ArrayList();
+    private String message = null;
+    private int status = HttpServletResponse.SC_OK;
+    private ServletOutputStream stream = null;
+    private PrintWriter writer = null;
+
+
+    // -------------------------------------------- HttpServletResponse Methods
+
+
+    /** {@inheritDoc} */
+    public void addCookie(Cookie cookie) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void addDateHeader(String name, long value) {
+
+        headers.add(name + ": " + formatDate(value));
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void addHeader(String name, String value) {
+
+        headers.add(name + ": " + value);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void addIntHeader(String name, int value) {
+
+        headers.add(name + ": " + value);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean containsHeader(String name) {
+
+        return getHeader(name) != null;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String encodeRedirectUrl(String url) {
+
+        return encodeRedirectURL(url);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String encodeRedirectURL(String url) {
+
+        return url;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String encodeUrl(String url) {
+
+        return encodeURL(url);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String encodeURL(String url) {
+
+        return url;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void sendError(int status) {
+
+        this.status = status;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void sendError(int status, String message) {
+
+        this.status = status;
+        this.message = message;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void sendRedirect(String location) {
+
+        this.status = HttpServletResponse.SC_MOVED_TEMPORARILY;
+        this.message = location;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setDateHeader(String name, long value) {
+
+        removeHeader(name);
+        addDateHeader(name, value);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setHeader(String name, String value) {
+
+        removeHeader(name);
+        addHeader(name, value);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setIntHeader(String name, int value) {
+
+        removeHeader(name);
+        addIntHeader(name, value);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setStatus(int status) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setStatus(int status, String message) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    // ------------------------------------------------ ServletResponse Methods
+
+
+    /** {@inheritDoc} */
+    public void flushBuffer() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int getBufferSize() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getCharacterEncoding() {
+
+        return this.encoding;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getContentType() {
+
+        return this.contentType;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Locale getLocale() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ServletOutputStream getOutputStream() throws IOException {
+
+        if (stream == null) {
+            if (writer != null) {
+                throw new IllegalStateException("Cannot call getOutputStream() after getWriter() has been called");
+            }
+            stream = new MockServletOutputStream(new ByteArrayOutputStream());
+        }
+        return stream;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public PrintWriter getWriter() throws IOException {
+
+        if (writer == null) {
+            if (stream != null) {
+                throw new IllegalStateException("Cannot call getWriter() after getOutputStream() was called");
+            }
+            writer = new MockPrintWriter(new CharArrayWriter());
+        }
+        return writer;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isCommitted() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void reset() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void resetBuffer() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setBufferSize(int size) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setCharacterEncoding(String charset) {
+
+        this.encoding = charset;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setContentLength(int length) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setContentType(String type) {
+
+        contentType = type;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setLocale(Locale locale) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    // --------------------------------------------------------- Private Methods
+
+
+    /**
+     * <p>The date formatting helper we will use in <code>httpTimestamp()</code>.
+     * Note that usage of this helper must be synchronized.</p>
+     */
+    private static SimpleDateFormat format =
+            new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
+    static {
+        format.setTimeZone(TimeZone.getTimeZone("GMT"));
+    }
+
+
+    /**
+     * <p>Return a properly formatted String version of the specified
+     * date/time, formatted as required by the HTTP specification.</p>
+     *
+     * @param date Date/time, expressed as milliseconds since the epoch
+     */
+    private String formatDate(long date) {
+        return format.format(new Date(date));
+    }
+
+
+    /**
+     * <p>Remove any header that has been set with the specific name.</p>
+     *
+     * @param name Header name to look up
+     */
+    private void removeHeader(String name) {
+        String match = name + ":";
+        Iterator headers = this.headers.iterator();
+        while (headers.hasNext()) {
+            String header = (String) headers.next();
+            if (header.startsWith(match)) {
+                headers.remove();
+                return;
+            }
+        }
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockHttpSession.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockHttpSession.java
new file mode 100644
index 0000000..e32a27c
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockHttpSession.java
@@ -0,0 +1,338 @@
+/*
+ * 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.shale.test.mock;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionContext;
+
+/**
+ * <p>Mock implementation of <code>HttpSession</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockHttpSession implements HttpSession {
+
+
+    // ------------------------------------------------------------ Constructors
+
+
+    /**
+     * <p>Configure a default instance.</p>
+     */
+    public MockHttpSession() {
+
+        super();
+
+    }
+
+
+    /**
+     * <p>Configure a session instance associated with the specified
+     * servlet context.</p>
+     *
+     * @param servletContext The associated servlet context
+     */
+    public MockHttpSession(ServletContext servletContext) {
+
+        super();
+        setServletContext(servletContext);
+
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    /**
+     * <p>Add a new listener instance that should be notified about
+     * attribute changes.</p>
+     *
+     * @param listener The new listener to be added
+     */
+    public void addAttributeListener(HttpSessionAttributeListener listener) {
+        attributeListeners.add(listener);
+    }
+
+
+    /**
+     * <p>Set the ServletContext associated with this session.</p>
+     *
+     * @param servletContext The associated servlet context
+     */
+    public void setServletContext(ServletContext servletContext) {
+
+        this.servletContext = servletContext;
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    private List attributeListeners = new ArrayList();
+    private HashMap attributes = new HashMap();
+    private String id = "123";
+    private ServletContext servletContext = null;
+
+
+    // ---------------------------------------------------------- Public Methods
+
+
+    /**
+     * <p>Set the session identifier of this session.</p>
+     *
+     * @param id The new session identifier
+     */
+    public void setId(String id) {
+        this.id = id;
+    }
+
+
+    // ----------------------------------------------------- HttpSession Methods
+
+
+    /** {@inheritDoc} */
+    public Object getAttribute(String name) {
+
+        return attributes.get(name);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getAttributeNames() {
+
+        return new MockEnumeration(attributes.keySet().iterator());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public long getCreationTime() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getId() {
+
+        return this.id;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public long getLastAccessedTime() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int getMaxInactiveInterval() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public ServletContext getServletContext() {
+
+        return this.servletContext;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public HttpSessionContext getSessionContext() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object getValue(String name) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String[] getValueNames() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void invalidate() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isNew() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void putValue(String name, Object value) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void removeAttribute(String name) {
+
+        if (attributes.containsKey(name)) {
+            Object value = attributes.remove(name);
+            fireAttributeRemoved(name, value);
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void removeValue(String name) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setAttribute(String name, Object value) {
+
+        if (name == null) {
+            throw new IllegalArgumentException("Attribute name cannot be null");
+        }
+        if (value == null) {
+            removeAttribute(name);
+            return;
+        }
+        if (attributes.containsKey(name)) {
+            Object oldValue = attributes.get(name);
+            attributes.put(name, value);
+            fireAttributeReplaced(name, oldValue);
+        } else {
+            attributes.put(name, value);
+            fireAttributeAdded(name, value);
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setMaxInactiveInterval(int interval) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    // --------------------------------------------------------- Support Methods
+
+
+    /**
+     * <p>Fire an attribute added event to interested listeners.</p>
+     *
+     * @param key Attribute whose value was added
+     * @param value The new value
+     */
+    private void fireAttributeAdded(String key, Object value) {
+        if (attributeListeners.size() < 1) {
+            return;
+        }
+        HttpSessionBindingEvent event =
+                new HttpSessionBindingEvent(this, key, value);
+        Iterator listeners = attributeListeners.iterator();
+        while (listeners.hasNext()) {
+            HttpSessionAttributeListener listener =
+                    (HttpSessionAttributeListener) listeners.next();
+            listener.attributeAdded(event);
+        }
+    }
+
+
+    /**
+     * <p>Fire an attribute removed event to interested listeners.</p>
+     *
+     * @param key Attribute whose value was removed
+     * @param value The removed value
+     */
+    private void fireAttributeRemoved(String key, Object value) {
+        if (attributeListeners.size() < 1) {
+            return;
+        }
+        HttpSessionBindingEvent event =
+                new HttpSessionBindingEvent(this, key, value);
+        Iterator listeners = attributeListeners.iterator();
+        while (listeners.hasNext()) {
+            HttpSessionAttributeListener listener =
+                    (HttpSessionAttributeListener) listeners.next();
+            listener.attributeRemoved(event);
+        }
+    }
+
+
+    /**
+     * <p>Fire an attribute replaced event to interested listeners.</p>
+     *
+     * @param key Attribute whose value was replaced
+     * @param value The original value
+     */
+    private void fireAttributeReplaced(String key, Object value) {
+        if (attributeListeners.size() < 1) {
+            return;
+        }
+        HttpSessionBindingEvent event =
+                new HttpSessionBindingEvent(this, key, value);
+        Iterator listeners = attributeListeners.iterator();
+        while (listeners.hasNext()) {
+            HttpSessionAttributeListener listener =
+                    (HttpSessionAttributeListener) listeners.next();
+            listener.attributeReplaced(event);
+        }
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockMethodBinding.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockMethodBinding.java
new file mode 100644
index 0000000..2a7a39c
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockMethodBinding.java
@@ -0,0 +1,249 @@
+/*
+ * 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.shale.test.mock;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import javax.faces.application.Application;
+import javax.faces.component.StateHolder;
+import javax.faces.component.UIComponentBase;
+import javax.faces.context.FacesContext;
+import javax.faces.el.EvaluationException;
+import javax.faces.el.MethodBinding;
+import javax.faces.el.MethodNotFoundException;
+import javax.faces.el.ReferenceSyntaxException;
+import javax.faces.el.ValueBinding;
+
+/**
+ * <p>Mock implementation of <code>MethodBinding</code>.</p>
+ *
+ * <p>This implementation is subject to the following restrictions:</p>
+ * <ul>
+ * <li>The portion of the method reference expression before the final
+ *     "." must conform to the limitations of {@link MockValueBinding}.</li>
+ * <li>The name of the method to be executed cannot be delimited by "[]".</li>
+ * </ul>
+ */
+
+public class MockMethodBinding extends MethodBinding implements StateHolder {
+
+
+    // ------------------------------------------------------------ Constructors
+
+    /**
+     * <p>Construct a default instance.</p>
+     */
+    public MockMethodBinding() {
+    }
+
+
+    /**
+     * <p>Construct a configured instance.</p>
+     *
+     * @param application Application instance for this application
+     * @param ref Method binding expression to be parsed
+     * @param args Signature of this method
+     */
+    public MockMethodBinding(Application application, String ref,
+                             Class[] args) {
+
+        this.application = application;
+        this.args = args;
+        if (ref.startsWith("#{") && ref.endsWith("}")) {
+            ref = ref.substring(2, ref.length() - 1);
+        }
+        this.ref = ref;
+        int period = ref.lastIndexOf(".");
+        if (period < 0) {
+            throw new ReferenceSyntaxException(ref);
+        }
+        vb = application.createValueBinding(ref.substring(0, period));
+        name = ref.substring(period + 1);
+        if (name.length() < 1) {
+            throw new ReferenceSyntaxException(ref);
+        }
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    private Application application;
+    private Class args[];
+    private String name;
+    private String ref;
+    private ValueBinding vb;
+
+
+    // --------------------------------------------------- MethodBinding Methods
+
+
+    /** {@inheritDoc} */
+    public Object invoke(FacesContext context, Object[] params)
+        throws EvaluationException, MethodNotFoundException {
+
+        if (context == null) {
+            throw new NullPointerException();
+        }
+        Object base = vb.getValue(context);
+        if (base == null) {
+            throw new EvaluationException("Cannot find object via expression \""
+                                          + vb.getExpressionString() + "\"");
+        }
+        Method method = method(base);
+        try {
+            return (method.invoke(base, params));
+        } catch (IllegalAccessException e) {
+            throw new EvaluationException(e);
+        } catch (InvocationTargetException e) {
+            throw new EvaluationException(e.getTargetException());
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Class getType(FacesContext context) {
+
+        Object base = vb.getValue(context);
+        Method method = method(base);
+        Class returnType = method.getReturnType();
+        if ("void".equals(returnType.getName())) {
+            return (null);
+        } else {
+            return (returnType);
+        }
+
+    }
+
+    /** {@inheritDoc} */
+    public String getExpressionString() {
+        return "#{" + ref + "}";
+    }
+
+    // ----------------------------------------------------- StateHolder Methods
+
+
+    /** {@inheritDoc} */
+    public Object saveState(FacesContext context) {
+        Object values[] = new Object[4];
+        values[0] = name;
+        values[1] = ref;
+        values[2] = UIComponentBase.saveAttachedState(context, vb);
+        values[3] = args;
+        return (values);
+    }
+
+
+    /** {@inheritDoc} */
+    public void restoreState(FacesContext context, Object state) {
+        Object values[] = (Object[]) state;
+        name = (String) values[0];
+        ref = (String) values[1];
+        vb = (ValueBinding) UIComponentBase.restoreAttachedState(context, 
+                                                                 values[2]);
+        args = (Class []) values[3];
+    }
+
+
+    /**
+     * <p>Flag indicating this is a transient instance.</p>
+     */
+    private boolean transientFlag = false;
+
+
+    /** {@inheritDoc} */
+    public boolean isTransient() {
+        return (this.transientFlag);
+    }
+
+
+    /** {@inheritDoc} */
+    public void setTransient(boolean transientFlag) {
+        this.transientFlag = transientFlag;
+    }
+
+    /** {@inheritDoc} */
+    public int hashCode() {
+        if (ref == null) {
+            return 0;
+        } else {
+            return ref.hashCode();
+        }
+    }
+
+    /** {@inheritDoc} */
+    public boolean equals(Object otherObj) {
+        MockMethodBinding other = null;
+
+        if (!(otherObj instanceof MockMethodBinding)) {
+            return false;
+        }
+        other = (MockMethodBinding) otherObj;
+        // test object reference equality
+        if (this.ref != other.ref) {
+            // test object equality
+            if (null != this.ref && null != other.ref) {
+                if (!this.ref.equals(other.ref)) {
+                    return false;
+                }
+            }
+            return false;
+        }
+        // no need to test name, since it flows from ref.
+        // test our args array
+        if (this.args != other.args) {
+            if (this.args.length != other.args.length) {
+                return false;
+            }
+            for (int i = 0, len = this.args.length; i < len; i++) {
+                if (this.args[i] != other.args[i]) {
+                    if (!this.ref.equals(other.ref)) {
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+
+    // --------------------------------------------------------- Private Methods
+
+
+    /**
+     * <p>Return the <code>Method</code> to be called.</p>
+     *
+     * @param base Base object from which to extract the method reference
+     */
+    Method method(Object base) {
+
+        Class clazz = base.getClass();
+        try {
+            return (clazz.getMethod(name, args));
+        } catch (NoSuchMethodException e) {
+            throw new MethodNotFoundException(ref + ": " + e.getMessage());
+        }
+
+    }
+
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockNavigationHandler.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockNavigationHandler.java
new file mode 100644
index 0000000..c93dbd0
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockNavigationHandler.java
@@ -0,0 +1,110 @@
+/*
+ * 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.shale.test.mock;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.faces.application.NavigationHandler;
+import javax.faces.application.ViewHandler;
+import javax.faces.component.UIViewRoot;
+import javax.faces.context.FacesContext;
+
+/**
+ * <p>Mock implementation of <code>NavigationHandler</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockNavigationHandler extends NavigationHandler {
+
+
+    // ------------------------------------------------------------ Constructors
+
+    /**
+     * <p>Construct a default instance.</p>
+     */
+    public MockNavigationHandler() {
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+
+    /**
+     * <p>Add a outcome-viewId pair to the destinations map.</p>
+     *
+     * @param outcome Logical outcome string
+     * @param viewId Destination view identifier
+     */
+    public void addDestination(String outcome, String viewId) {
+
+        destinations.put(outcome, viewId);
+
+    }
+
+
+    // ------------------------------------------------------ Instance Variables
+
+
+    /**
+     * <p>Set of destination view ids, keyed by logical outcome String
+     * that will cause navigation to that view id.</p>
+     */
+    private Map destinations = new HashMap();
+
+
+    // ----------------------------------------------- NavigationHandler Methods
+
+
+    /**
+     * <p>Process the specified navigation request.</p>
+     *
+     * @param context <code>FacesContext</code> for the current request
+     * @param action Action method being executed
+     * @param outcome Logical outcome from this action method
+     */
+    public void handleNavigation(FacesContext context,
+                                 String action, String outcome) {
+
+        // Navigate solely based on outcome, if we get a match
+        String viewId = (String) destinations.get(outcome);
+        if (viewId != null) {
+            UIViewRoot view = getViewHandler(context).createView(context, viewId);
+            context.setViewRoot(view);
+        }
+
+    }
+
+
+    // --------------------------------------------------------- Private Methods
+
+
+    /**
+     * <p>Return the <code>ViewHandler</code> instance for this application.</p>
+     *
+     * @param context <code>FacesContext</code> for the current request
+     */
+    private ViewHandler getViewHandler(FacesContext context) {
+
+        return context.getApplication().getViewHandler();
+
+    }
+
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletContext.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletContext.java
new file mode 100644
index 0000000..fcd24a8
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletContext.java
@@ -0,0 +1,326 @@
+/*
+ * 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.shale.test.mock;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Set;
+
+import javax.portlet.PortletContext;
+import javax.portlet.PortletRequestDispatcher;
+
+/**
+ * <p>Mock implementation of <code>PortletContext</code>.</p>
+ *
+ * $Id$
+ */
+public class MockPortletContext implements PortletContext {
+
+    // ----------------------------------------------------- Mock Object Methods
+
+    /**
+     * <p>Add a context initialization parameter to the set of parameters
+     * recognized by this instance.</p>
+     *
+     * @param name Parameter name
+     * @param value Parameter value
+     */
+    public void addInitParameter(String name, String value) {
+
+        parameters.put(name, value);
+
+    }
+
+
+    /**
+     * <p>Add a new MIME type mapping to the set of mappings recognized by this
+     * instance.</p>
+     * 
+     * @param extension Extension to check for (without the period)
+     * @param contentType Corresponding content type
+     */
+    public void addMimeType(String extension, String contentType) {
+
+        mimeTypes.put(extension, contentType);
+
+    }
+
+
+    /**
+     * <p>Set the document root for <code>getRealPath()</code> resolution.
+     * This parameter <strong>MUST</strong> represent a directory.</p>
+     *
+     * @param documentRoot The new base directory
+     */
+    public void setDocumentRoot(File documentRoot) {
+
+        this.documentRoot = documentRoot;
+
+    }
+
+    // ------------------------------------------------------ Instance Variables
+
+    private Hashtable attributes = new Hashtable();
+    private File documentRoot = null;
+    private Hashtable mimeTypes = new Hashtable();
+    private Hashtable parameters = new Hashtable();
+
+
+    // -------------------------------------------------- PortletContext Methods
+
+
+    /** {@inheritDoc} */
+    public Object getAttribute(String name) {
+
+        return attributes.get(name);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getAttributeNames() {
+
+        return attributes.keys();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getInitParameter(String name) {
+
+        return (String) parameters.get(name);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getInitParameterNames() {
+
+        return parameters.keys();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int getMajorVersion() {
+
+        return 1;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getMimeType(String path) {
+
+        int period = path.lastIndexOf('.');
+        if (period < 0) {
+            return null;
+        }
+        String extension = path.substring(period + 1);
+        return (String) mimeTypes.get(extension);
+
+    }
+
+
+    public int getMinorVersion() {
+
+        // TODO Auto-generated method stub
+        return 0;
+    }
+
+
+    public PortletRequestDispatcher getNamedDispatcher(String arg0) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getPortletContextName() {
+
+        return "MockPortletContext";
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRealPath(String path) {
+
+        if (documentRoot != null) {
+            if (!path.startsWith("/")) {
+                throw new IllegalArgumentException("The specified path ('"
+                        + path + "') does not start with a '/' character");
+            }
+            File resolved = new File(documentRoot, path.substring(1));
+            try {
+                return resolved.getCanonicalPath();
+            } catch (IOException e) {
+                return resolved.getAbsolutePath();
+            }
+        } else {
+            return null;
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public PortletRequestDispatcher getRequestDispatcher(String arg0) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public URL getResource(String path) throws MalformedURLException {
+
+        if (documentRoot != null) {
+            if (!path.startsWith("/")) {
+                throw new MalformedURLException("The specified path ('" + path
+                        + "') does not start with a '/' character");
+            }
+            File resolved = new File(documentRoot, path.substring(1));
+            if (resolved.exists()) {
+                return resolved.toURL();
+            } else {
+                return null;
+            }
+        } else {
+            return null;
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public InputStream getResourceAsStream(String path) {
+
+        try {
+            URL url = getResource(path);
+            if (url != null) {
+                return url.openStream();
+            }
+        } catch (Exception e) {
+            ;
+        }
+        return null;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Set getResourcePaths(String path) {
+
+        if (documentRoot == null) {
+            return null;
+        }
+
+        // Enforce the leading slash restriction
+        if (!path.startsWith("/")) {
+            throw new IllegalArgumentException("The specified path ('" + path
+                    + "') does not start with a '/' character");
+        }
+
+        // Locate the File node for this path's directory (if it exists)
+        File node = new File(documentRoot, path.substring(1));
+        if (!node.exists()) {
+            return null;
+        }
+        if (!node.isDirectory()) {
+            return null;
+        }
+
+        // Construct a Set containing the paths to the contents of this
+        // directory
+        Set set = new HashSet();
+        String[] files = node.list();
+        if (files == null) {
+            return null;
+        }
+        for (int i = 0; i < files.length; i++) {
+            String subfile = path + files[i];
+            File subnode = new File(node, files[i]);
+            if (subnode.isDirectory()) {
+                subfile += "/";
+            }
+            set.add(subfile);
+        }
+
+        // Return the completed set
+        return set;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getServerInfo() {
+
+        return "MockPortletContext";
+    }
+
+
+    /** {@inheritDoc} */
+    public void log(String message) {
+
+        System.out.println(message);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void log(String message, Throwable exception) {
+
+        System.out.println(message);
+        exception.printStackTrace();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void removeAttribute(String name) {
+
+        if (attributes.containsKey(name)) {
+            attributes.remove(name);
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setAttribute(String name, Object value) {
+
+        if (name == null) {
+            throw new IllegalArgumentException("Attribute name cannot be null");
+        }
+        if (value == null) {
+            removeAttribute(name);
+            return;
+        }
+        attributes.put(name, value);
+
+    }
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletRequest.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletRequest.java
new file mode 100644
index 0000000..83c96b5
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletRequest.java
@@ -0,0 +1,421 @@
+/*
+ * 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.shale.test.mock;
+
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.portlet.PortalContext;
+import javax.portlet.PortletMode;
+import javax.portlet.PortletPreferences;
+import javax.portlet.PortletRequest;
+import javax.portlet.PortletSession;
+import javax.portlet.WindowState;
+
+/**
+ * <p> Mock implementation of <code>PortletRequest</code>. </p>
+ *
+ * $Id$
+ */
+
+public class MockPortletRequest implements PortletRequest {
+
+    // ------------------------------------------------------------ Constructors
+
+    public MockPortletRequest() {
+
+        super();
+
+    }
+
+
+    public MockPortletRequest(PortletSession session) {
+
+        super();
+        this.session = session;
+
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+    /**
+     * <p> Add a request parameter for this request. </p>
+     *
+     * @param name Parameter name
+     * @param value Parameter value
+     */
+    public void addParameter(String name, String value) {
+
+        String[] values = (String[]) parameters.get(name);
+        if (values == null) {
+            String[] results = new String[] { value };
+            parameters.put(name, results);
+            return;
+        }
+        String[] results = new String[values.length + 1];
+        System.arraycopy(values, 0, results, 0, values.length);
+        results[values.length] = value;
+        parameters.put(name, results);
+
+    }
+
+
+    /**
+     * <p> Set the <code>PortletSession</code> associated with this request.
+     * </p>
+     *
+     * @param session The new session
+     */
+    public void setPortletSession(PortletSession session) {
+
+        this.session = session;
+    }
+
+
+    /**
+     * <p> Set the <code>Locale</code> associated with this request. </p>
+     *
+     * @param locale The new locale
+     */
+    public void setLocale(Locale locale) {
+
+        this.locale = locale;
+
+    }
+
+
+    /**
+     * <p> Set the <code>Principal</code> associated with this request. </p>
+     *
+     * @param principal The new Principal
+     */
+    public void setUserPrincipal(Principal principal) {
+
+        this.principal = principal;
+
+    }
+
+    // ------------------------------------------------------ Instance Variables
+
+    private Map attributes = new HashMap();
+    private String contextPath = null;
+    private Locale locale = null;
+    private Map parameters = new HashMap();
+    private Principal principal = null;
+    private PortletSession session = null;
+
+
+    // -------------------------------------------------- PortletRequest Methods
+
+
+    /** {@inheritDoc} */
+    public String getAuthType() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getContextPath() {
+
+        return contextPath;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object getAttribute(String name) {
+
+        return attributes.get(name);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getAttributeNames() {
+
+        return new MockEnumeration(attributes.keySet().iterator());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Locale getLocale() {
+
+        return locale;
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getLocales() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getParameter(String name) {
+
+        String[] values = (String[]) parameters.get(name);
+        if (values != null) {
+            return values[0];
+        } else {
+            return null;
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Map getParameterMap() {
+
+        return parameters;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getParameterNames() {
+
+        return new MockEnumeration(parameters.keySet().iterator());
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String[] getParameterValues(String name) {
+
+        return (String[]) parameters.get(name);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public PortalContext getPortalContext() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public PortletMode getPortletMode() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public PortletSession getPortletSession() {
+
+        return getPortletSession(true);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public PortletSession getPortletSession(boolean create) {
+
+        if (create && (session == null)) {
+            throw new UnsupportedOperationException();
+        }
+        return session;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public PortletPreferences getPreferences() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getProperties(String arg0) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getProperty(String arg0) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getPropertyNames() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRemoteUser() {
+
+        if (principal != null) {
+            return principal.getName();
+        } else {
+            return null;
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getRequestedSessionId() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getResponseContentType() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getResponseContentTypes() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getScheme() {
+
+        return ("http");
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String getServerName() {
+
+        return ("localhost");
+
+    }
+
+
+    /** {@inheritDoc} */
+    public int getServerPort() {
+
+        return (8080);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Principal getUserPrincipal() {
+
+        return principal;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public WindowState getWindowState() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isPortletModeAllowed(PortletMode arg0) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isRequestedSessionIdValid() {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isSecure() {
+
+        return false;
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isUserInRole(String arg0) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isWindowStateAllowed(WindowState arg0) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void removeAttribute(String name) {
+
+        if (attributes.containsKey(name)) {
+            attributes.remove(name);
+        }
+
+    }
+
+
+    /** {@inheritDoc} */
+    public void setAttribute(String name, Object value) {
+
+        if (name == null) {
+            throw new IllegalArgumentException("Attribute name cannot be null");
+        }
+        if (value == null) {
+            removeAttribute(name);
+            return;
+        }
+        attributes.put(name, value);
+
+    }
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletResponse.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletResponse.java
new file mode 100644
index 0000000..b3ae1ea
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletResponse.java
@@ -0,0 +1,62 @@
+/*
+ * 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.shale.test.mock;
+
+import javax.portlet.PortletResponse;
+
+/**
+ * <p>Mock implementation of <code>PortletResponse</code>.</p>
+ *
+ * $Id$
+ */
+
+public class MockPortletResponse implements PortletResponse {
+
+    /**
+     * <p>Return a default instance.</p>
+     */
+    public MockPortletResponse() {
+
+    }
+
+
+    // -------------------------------------------------- PortletContext Methods
+
+    /** {@inheritDoc} */
+    public void addProperty(String name, String value) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+
+    /** {@inheritDoc} */
+    public String encodeURL(String url) {
+
+        return url;
+    }
+
+
+    /** {@inheritDoc} */
+    public void setProperty(String name, String value) {
+
+        throw new UnsupportedOperationException();
+
+    }
+
+}
diff --git a/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletSession.java b/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletSession.java
new file mode 100644
index 0000000..96d182b
--- /dev/null
+++ b/shale-test/src/main/java/org/apache/shale/test/mock/MockPortletSession.java
@@ -0,0 +1,268 @@
+/*
+ * 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.shale.test.mock;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.portlet.PortletContext;
+import javax.portlet.PortletSession;
+
+/**
+ * <p> Mock implementation of <code>PortletSession</code>. </p>
+ * 
+ * $Id$
+ */
+public class MockPortletSession implements PortletSession {
+
+    // ------------------------------------------------------------ Constructors
+
+    /**
+     * <p> Configure a default instance. </p>
+     */
+    public MockPortletSession() {
+
+        super();
+
+    }
+
+
+    /**
+     * <p> Configure a session instance associated with the specified servlet
+     * context. </p>
+     *
+     * @param servletContext The associated servlet context
+     */
+    public MockPortletSession(PortletContext portletContext) {
+
+        super();
+        this.portletContext = portletContext;
+
+    }
+
+
+    // ----------------------------------------------------- Mock Object Methods
+
+    /**
+     * <p> Set the <code>PortletContext</code> associated with this session.
+     * </p>
+     *
+     * @param servletContext The associated servlet context
+     */
+    public void setPortletContext(PortletContext portletContext) {
+
+        this.portletContext = portletContext;
+
+    }
+
+    // ------------------------------------------------------ Instance Variables
+
+    private Map portletAttributes = new HashMap();
+    private Map applicationAttributes = new HashMap();
+    private String id = "123";
+    private PortletContext portletContext = null;
+
+
+    // ---------------------------------------------------------- Public Methods
+
+    /**
+     * <p> Set the session identifier of this session. </p>
+     *
+     * @param id The new session identifier
+     */
+    public void setId(String id) {
+
+        this.id = id;
+
+    }
+
+
+    // -------------------------------------------------- PortletSession Methods
+
+
+    /** {@inheritDoc} */
+    public Object getAttribute(String name) {
+
+        return getAttribute(name, PORTLET_SCOPE);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Object getAttribute(String name, int scope) {
+
+        if (scope == PORTLET_SCOPE) {
+            return portletAttributes.get(name);
+        } else if (scope == APPLICATION_SCOPE) {
+            return applicationAttributes.get(name);
+        }
+
+        throw new IllegalArgumentException("Scope constant " + scope
+                + " not recognized");
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getAttributeNames() {
+
+        return getAttributeNames(PORTLET_SCOPE);
+
+    }
+
+
+    /** {@inheritDoc} */
+    public Enumeration getAttributeNames(int scope) {
+
+        if (scope == PORTLET_SCOPE) {
+            return new MockEnumeration(portletAttributes.keySet().iterator());
+        } else if (scope == APPLICATION_SCOPE) {
+            return new MockEnumeration(applicationAttributes.keySet()
+                    .iterator());
+        }
+
+        throw new IllegalArgumentException("Scope constant " + scope
+                + " not recognized");
+
+    }
+
... 6108 lines suppressed ...

-- 
To stop receiving notification emails like this one, please contact
deki@apache.org.