diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/web.xml
index ccf8949a..d493df31 100644
--- a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/web.xml
+++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/web.xml
@@ -23,6 +23,18 @@
org.apache.myfaces.webapp.StartupServletContextListener
+
+
+ Flow System Cleanup Filter
+ org.springframework.webflow.executor.jsf.FlowSystemCleanupFilter
+
+
+
+
+ Flow System Cleanup Filter
+ *.faces
+
+
Faces Servlet
diff --git a/spring-webflow/changelog.txt b/spring-webflow/changelog.txt
index 6d7c2a02..899d33cd 100644
--- a/spring-webflow/changelog.txt
+++ b/spring-webflow/changelog.txt
@@ -34,6 +34,8 @@ Package org.springframework.webflow.executor
be matched against the state of the current flow execution (SWF-303).
* JSF integration code now tears down ExternalContext thread local properly in exceptional situations and when
the RENDER_RESPONSE phase is bypassed (SWF-305).
+* JSF can now use a FlowSystemCleanupFilter to ensure that no matter what happens in JSF, the request context
+ is cleaned up properly (SWF-306).
Changes in version 1.0.3 (19.04.2007)
-------------------------------------
diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowSystemCleanupFilter.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowSystemCleanupFilter.java
new file mode 100644
index 00000000..c1f0b84a
--- /dev/null
+++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowSystemCleanupFilter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2004-2007 the original author or authors.
+ *
+ * Licensed 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.springframework.webflow.executor.jsf;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.webflow.context.ExternalContextHolder;
+import org.springframework.webflow.execution.FlowExecutionContextHolder;
+
+/**
+ * A servlet filter used to guarantee that webflow context information is
+ * cleaned up in a JSF environment.
+ *
+ * @author Ben Hale
+ * @since 1.1
+ */
+public class FlowSystemCleanupFilter extends OncePerRequestFilter {
+
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws ServletException, IOException {
+ try {
+ chain.doFilter(request, response);
+ } finally {
+ cleanupCurrentFlowExecution(request);
+ ExternalContextHolder.setExternalContext(null);
+ }
+ }
+
+ /**
+ * Cleans up the current flow execution in the request context if necessary.
+ * Specifically, handles unlocking the execution if necessary, setting the
+ * holder to null, and cleaning up the flow execution context thread local.
+ * Can be safely called even if no execution is bound or one is bound but
+ * not locked.
+ * @param request the servlet request
+ */
+ private void cleanupCurrentFlowExecution(ServletRequest request) {
+ if (isFlowExecutionRestored(request)) {
+ FlowExecutionContextHolder.setFlowExecutionContext(null);
+ getFlowExecutionHolder(request).unlockFlowExecutionIfNecessary();
+ request.removeAttribute(getFlowExecutionHolderKey());
+ }
+ }
+
+ /**
+ * Returns true if the flow execution has been restored in the current
+ * thread.
+ * @param request the servlet request
+ * @return true if restored, false otherwise
+ */
+ private boolean isFlowExecutionRestored(ServletRequest request) {
+ return getFlowExecutionHolder(request) != null;
+ }
+
+ /**
+ * Returns the current flow execution holder for the given servlet request.
+ * @param request the servlet request
+ * @return the flow execution holder, or null if none set.
+ */
+ private FlowExecutionHolder getFlowExecutionHolder(ServletRequest request) {
+ return (FlowExecutionHolder) request.getAttribute(getFlowExecutionHolderKey());
+ }
+
+ private static String getFlowExecutionHolderKey() {
+ return FlowExecutionHolder.class.getName();
+ }
+
+}
diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowSystemCleanupFilterTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowSystemCleanupFilterTests.java
new file mode 100644
index 00000000..ec3dbf8e
--- /dev/null
+++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowSystemCleanupFilterTests.java
@@ -0,0 +1,85 @@
+package org.springframework.webflow.executor.jsf;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import junit.framework.TestCase;
+
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.webflow.context.ExternalContextHolder;
+import org.springframework.webflow.engine.impl.FlowExecutionImpl;
+import org.springframework.webflow.execution.FlowExecutionContextHolder;
+import org.springframework.webflow.test.MockExternalContext;
+
+public class FlowSystemCleanupFilterTests extends TestCase {
+
+ private FlowSystemCleanupFilter filter;
+
+ private ServletRequest request;
+
+ private ServletResponse response;
+
+ private FilterChain chain;
+
+ protected void setUp() throws Exception {
+ filter = new FlowSystemCleanupFilter();
+ request = new MockHttpServletRequest();
+ request.setAttribute(getFlowExecutionHolderKey(), new FlowExecutionHolder(new FlowExecutionImpl()));
+ response = new MockHttpServletResponse();
+ chain = new MockFilterChain();
+ FlowExecutionContextHolder.setFlowExecutionContext(new FlowExecutionImpl());
+ ExternalContextHolder.setExternalContext(new MockExternalContext());
+ }
+
+ public void testCleanup() throws ServletException, IOException {
+ filter.doFilter(request, response, chain);
+
+ assertNull("Should have cleaned up the flow execution", request.getAttribute(getFlowExecutionHolderKey()));
+ try {
+ FlowExecutionContextHolder.getFlowExecutionContext();
+ fail("Should have an empty holder");
+ } catch (IllegalStateException e) {
+ }
+ try {
+ ExternalContextHolder.getExternalContext();
+ fail("Should have an empty holder");
+ } catch (IllegalStateException e) {
+ }
+ }
+
+ public void testExceptionThrown() throws ServletException, IOException {
+ try {
+ filter.doFilter(request, response, new ExceptionThrowingMockFilterChain());
+ } catch (RuntimeException e) {
+ assertNull("Should have cleaned up the flow execution", request.getAttribute(getFlowExecutionHolderKey()));
+ try {
+ FlowExecutionContextHolder.getFlowExecutionContext();
+ fail("Should have an empty holder");
+ } catch (IllegalStateException e1) {
+ }
+ try {
+ ExternalContextHolder.getExternalContext();
+ fail("Should have an empty holder");
+ } catch (IllegalStateException e1) {
+ }
+ }
+ }
+
+ private static String getFlowExecutionHolderKey() {
+ return FlowExecutionHolder.class.getName();
+ }
+
+ private class ExceptionThrowingMockFilterChain extends MockFilterChain {
+
+ public void doFilter(ServletRequest request, ServletResponse response) {
+ throw new RuntimeException();
+ }
+
+ }
+}