From 4a947948d2321755454ee093199a7ad5319ed866 Mon Sep 17 00:00:00 2001 From: Ben Hale Date: Wed, 2 May 2007 20:42:22 +0000 Subject: [PATCH] Added Spring scoping support (SWF-163). Also updated the sellitem-jsf sample to use the new scoping scheme. --- .../src/main/webapp/WEB-INF/faces-config.xml | 2 +- .../webapp/WEB-INF/flows/sellitem-beans.xml | 15 + .../webapp/WEB-INF/flows/sellitem-flow.xml | 4 +- .../src/main/webapp/WEB-INF/web.xml | 1 + .../src/main/java/META-INF/spring.schemas | 3 +- .../EnableScopesBeanDefinitionParser.java | 39 ++ .../config/WebFlowConfigNamespaceHandler.java | 1 + .../config/spring-webflow-config-1.1.xsd | 356 ++++++++++++++++++ .../context/scope/AbstractWebFlowScope.java | 125 ++++++ .../context/scope/ConversationScope.java | 35 ++ .../webflow/context/scope/FlashScope.java | 35 ++ .../webflow/context/scope/FlowScope.java | 35 ++ .../webflow/context/scope/ScopeRegistrar.java | 43 +++ .../context/scope/ScopedBeanException.java | 47 +++ .../execution/FlowExecutionContextHolder.java | 63 ++++ .../jsf/FlowExecutionHolderUtils.java | 2 + .../executor/jsf/FlowNavigationHandler.java | 2 + .../executor/jsf/FlowPhaseListener.java | 2 + .../context/scope/ConversationScopeTests.java | 105 ++++++ .../context/scope/FlashScopeTests.java | 108 ++++++ .../webflow/context/scope/FlowScopeTests.java | 106 ++++++ .../context/scope/StubObjectFactory.java | 38 ++ 22 files changed, 1162 insertions(+), 5 deletions(-) create mode 100644 spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-beans.xml create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/config/EnableScopesBeanDefinitionParser.java create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/config/spring-webflow-config-1.1.xsd create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/context/scope/AbstractWebFlowScope.java create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/context/scope/ConversationScope.java create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/context/scope/FlashScope.java create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/context/scope/FlowScope.java create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/context/scope/ScopeRegistrar.java create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/context/scope/ScopedBeanException.java create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionContextHolder.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/context/scope/ConversationScopeTests.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/context/scope/FlashScopeTests.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/context/scope/FlowScopeTests.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/context/scope/StubObjectFactory.java diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/faces-config.xml b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/faces-config.xml index a040566f..31528b85 100644 --- a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/faces-config.xml +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/faces-config.xml @@ -6,7 +6,7 @@ org.springframework.webflow.executor.jsf.FlowNavigationHandler - org.springframework.webflow.executor.jsf.DelegatingFlowVariableResolver + org.springframework.web.jsf.DelegatingVariableResolver diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-beans.xml b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-beans.xml new file mode 100644 index 00000000..099b0558 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-beans.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-flow.xml b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-flow.xml index 5bbce140..35a7e89d 100644 --- a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-flow.xml +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-flow.xml @@ -3,8 +3,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-1.0.xsd"> - - @@ -33,6 +31,6 @@ - + \ No newline at end of file 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..37a665b0 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 @@ -9,6 +9,7 @@ classpath:org/springframework/webflow/samples/sellitem/services-config.xml /WEB-INF/webflow-config.xml + /WEB-INF/flows/sellitem-beans.xml diff --git a/spring-webflow/src/main/java/META-INF/spring.schemas b/spring-webflow/src/main/java/META-INF/spring.schemas index f787288c..3aa513c6 100644 --- a/spring-webflow/src/main/java/META-INF/spring.schemas +++ b/spring-webflow/src/main/java/META-INF/spring.schemas @@ -1 +1,2 @@ -http\://www.springframework.org/schema/webflow-config/spring-webflow-config-1.0.xsd=org/springframework/webflow/config/spring-webflow-config-1.0.xsd \ No newline at end of file +http\://www.springframework.org/schema/webflow-config/spring-webflow-config-1.0.xsd=org/springframework/webflow/config/spring-webflow-config-1.0.xsd +http\://www.springframework.org/schema/webflow-config/spring-webflow-config-1.1.xsd=org/springframework/webflow/config/spring-webflow-config-1.1.xsd \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/EnableScopesBeanDefinitionParser.java b/spring-webflow/src/main/java/org/springframework/webflow/config/EnableScopesBeanDefinitionParser.java new file mode 100644 index 00000000..8f7c44a4 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/EnableScopesBeanDefinitionParser.java @@ -0,0 +1,39 @@ +/* + * 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.config; + +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.webflow.context.scope.ScopeRegistrar; +import org.w3c.dom.Element; + +/** + * {@link BeanDefinitionParser} for the <enable-scopes> tag. + * + * @author Ben Hale + * @since 1.1 + */ +class EnableScopesBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + protected Class getBeanClass(Element element) { + return ScopeRegistrar.class; + } + + protected boolean shouldGenerateId() { + return true; + } + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/WebFlowConfigNamespaceHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/config/WebFlowConfigNamespaceHandler.java index bf184761..50e2d09d 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/config/WebFlowConfigNamespaceHandler.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/WebFlowConfigNamespaceHandler.java @@ -65,5 +65,6 @@ public class WebFlowConfigNamespaceHandler extends NamespaceHandlerSupport { registerBeanDefinitionParser("execution-listeners", new ExecutionListenersBeanDefinitionParser()); registerBeanDefinitionParser("executor", new ExecutorBeanDefinitionParser()); registerBeanDefinitionParser("registry", new RegistryBeanDefinitionParser()); + registerBeanDefinitionParser("enable-scopes", new EnableScopesBeanDefinitionParser()); } } \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/spring-webflow-config-1.1.xsd b/spring-webflow/src/main/java/org/springframework/webflow/config/spring-webflow-config-1.1.xsd new file mode 100644 index 00000000..5a04104c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/spring-webflow-config-1.1.xsd @@ -0,0 +1,356 @@ + + + + + + +Provides an easy way to configure a flow executor and an XML flow definition registry. +]]> + + + + + + + + + + + + + + + + + +Each flow definition registered in this registry is assigned a unique identifier. By default, +this identifier is the name of the externalized resource minus its file extension. For example, +a registry containing flow definitions built from the files "orderitem-flow.xml" and "shipping-flow.xml" +would index those definitions by "orderitem-flow" and "shipping-flow" by default. +
+A flow registry is used by a flow executor at runtime to launch new executions of flow definitions. +]]> +
+
+ + + + + + + + +Individual paths such as: +
+	/WEB-INF/flows/orderitem-flow.xml
+
+... are supported as well as wildcard paths such as: +
+	/WEB-INF/flows/**/*-flow.xml
+
+]]> +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/scope/AbstractWebFlowScope.java b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/AbstractWebFlowScope.java new file mode 100644 index 00000000..290eae65 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/AbstractWebFlowScope.java @@ -0,0 +1,125 @@ +/* + * 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.context.scope; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.FlowExecutionContextHolder; +import org.springframework.webflow.execution.RequestContext; + +/** + * Abstract {@link Scope} implementation that reads from a particular scope + * in the current thread-bound {@link FlowExecutionContext} object. + * + *

Subclasses simply need to implement {@link #getScope()} to instruct + * this class which {@link FlowExecutionContext} scope to read attributes from. + * + *

Relies on a thread-bound @{link FlowExecutionContext} instance wich is located + * through a @{link FlowExecutionContextHolder}. + * + * @author Ben Hale + * @since 1.1 + * @see FlowExecutionContext + * @see FlowExecutionContextHolder + */ +public abstract class AbstractWebFlowScope implements Scope { + + /** + * Logger, usable by subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Template method that determines the actual target scope. + * @return the target scope + * @see RequestContext#getConversationScope() + * @see RequestContext#getFlowScope() + * @see RequestContext#getRequestScope() + * @see RequestContext#getFlashScope() + */ + protected abstract MutableAttributeMap getScope(); + + public Object get(String name, ObjectFactory objectFactory) { + MutableAttributeMap scope; + try { + scope = getScope(); + } catch(IllegalStateException e) { + throw new ScopedBeanException("Cannot retrieve scoped bean '" + + name + "' before the scope has been populated"); + } + Object scopedObject = scope.get(name); + if (scopedObject == null) { + if(logger.isDebugEnabled()) { + logger.debug("Could not find existing scoped instance of '" + + name + "'; creating new instace"); + } + scopedObject = objectFactory.getObject(); + scope.put(name, scopedObject); + } else { + if(logger.isDebugEnabled()) { + logger.debug("Found existing scoped instance of '" + + name + "'"); + } + } + return scopedObject; + } + + public Object remove(String name) { + try { + return getScope().remove(name); + } catch(IllegalStateException e) { + throw new ScopedBeanException("Cannot remove scoped bean '" + + name + "' before the scope has been populated"); + } + } + + /** + * Always returns null as most Spring Web Flow scopes do not + * have obvious conversation ids. Subclasses should override this method + * where conversation ids can be intelligently returned. + * @return always returns null + */ + public String getConversationId() { + return null; + } + + /** + * Will not register a destruction callback as Spring Web Flow does not + * support destruction of scoped beans. Subclasses should override this + * method where where destruction can adequately be accomplished. + * @param name the name of the bean to register the callback for + * @param callback the callback to execute + */ + public void registerDestructionCallback(String name, Runnable callback) { + logger.warn("Destruction callback for '" + name + + "' was not registered. Spring Web Flow does not " + + "support destruction of scoped beans."); + } + + /** + * Returns the current flow execution context. Used by subclasses to easily + * get access to the thread-bound flow execution context. + * @return the current thread-bound flow execution context + */ + protected FlowExecutionContext getFlowExecutionContext() { + return FlowExecutionContextHolder.getFlowExecutionContext(); + } + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/scope/ConversationScope.java b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/ConversationScope.java new file mode 100644 index 00000000..15c34fa1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/ConversationScope.java @@ -0,0 +1,35 @@ +/* + * 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.context.scope; + +import org.springframework.beans.factory.config.Scope; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.FlowExecution; + +/** + * Conversation-backed {@link Scope} implementation. + * + * @author Ben Hale + * @since 1.1 + * @see FlowExecution#getConversationScope() + */ +public class ConversationScope extends AbstractWebFlowScope { + + protected MutableAttributeMap getScope() { + return getFlowExecutionContext().getConversationScope(); + } + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/scope/FlashScope.java b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/FlashScope.java new file mode 100644 index 00000000..4b8ea80f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/FlashScope.java @@ -0,0 +1,35 @@ +/* + * 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.context.scope; + +import org.springframework.beans.factory.config.Scope; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.FlowSession; + +/** + * Flash-backed {@link Scope} implementation. + * + * @author Ben Hale + * @since 1.1 + * @see FlowSession#getFlashMap() + */ +public class FlashScope extends AbstractWebFlowScope { + + protected MutableAttributeMap getScope() { + return getFlowExecutionContext().getActiveSession().getFlashMap(); + } + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/scope/FlowScope.java b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/FlowScope.java new file mode 100644 index 00000000..1d6d8d43 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/FlowScope.java @@ -0,0 +1,35 @@ +/* + * 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.context.scope; + +import org.springframework.beans.factory.config.Scope; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.FlowSession; + +/** + * Flow-backed {@link Scope} implementation. + * + * @author Ben Hale + * @since 1.1 + * @see FlowSession#getScope() + */ +public class FlowScope extends AbstractWebFlowScope { + + protected MutableAttributeMap getScope() { + return getFlowExecutionContext().getActiveSession().getScope(); + } + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/scope/ScopeRegistrar.java b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/ScopeRegistrar.java new file mode 100644 index 00000000..9d9cdf8e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/ScopeRegistrar.java @@ -0,0 +1,43 @@ +/* + * 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.context.scope; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.core.Ordered; +import org.springframework.webflow.execution.ScopeType; + +/** + * Registers the Spring Web Flow bean scopes with a @{link ConfigurableListableBeanFactory}. + * + * @author Ben Hale + * @see Scope + */ +public class ScopeRegistrar implements BeanFactoryPostProcessor, Ordered { + + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + beanFactory.registerScope(ScopeType.CONVERSATION.getLabel().toLowerCase(), new ConversationScope()); + beanFactory.registerScope(ScopeType.FLASH.getLabel().toLowerCase(), new FlashScope()); + beanFactory.registerScope(ScopeType.FLOW.getLabel().toLowerCase(), new FlowScope()); + } + + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/scope/ScopedBeanException.java b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/ScopedBeanException.java new file mode 100644 index 00000000..0199a74f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/scope/ScopedBeanException.java @@ -0,0 +1,47 @@ +/* + * 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.context.scope; + +import org.springframework.webflow.core.FlowException; + +/** + * Thrown when a bean cannot be retrieved because the scope it resides in + * has not yet been populated. This might occur if the bean is being referenced + * before the FlowExecution has been retrieved. + * + * @author Ben Hale + * @since 1.1 + */ +public class ScopedBeanException extends FlowException { + + /** + * Create a new scoped bean exception. + * @param message the message + * @param cause the root cause of the failure + */ + public ScopedBeanException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Create a new scoped bean exception. + * @param message the message + */ + public ScopedBeanException(String message) { + super(message); + } + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionContextHolder.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionContextHolder.java new file mode 100644 index 00000000..bef4aee0 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionContextHolder.java @@ -0,0 +1,63 @@ +/* + * 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.execution; + +import org.springframework.util.Assert; + +/** + * Simple holder class that associates a {@link FlowExecutionContext} instance + * with the current thread. The FlowExecutionContext will not be inherited by + * any child threads spawned by the current thread. + *

+ * Used as a central holder for the current FlowExecutionContext in Spring Web + * Flow, wherever necessary. Often used by artifacts needing to access the + * current active flow execution. + * + * @see FlowExecutionContext + * + * @author Ben Hale + * @since 1.1 + */ +public class FlowExecutionContextHolder { + + private static final ThreadLocal flowExecutionContextHolder = new ThreadLocal(); + + /** + * Associate the given FlowExecutionContext with the current thread. + * @param flowExecutionContext the current FlowExecutionContext, or + * null to reset the thread-bound context + */ + public static void setFlowExecutionContext(FlowExecutionContext flowExecutionContext) { + flowExecutionContextHolder.set(flowExecutionContext); + } + + /** + * Return the FlowExecutionContext associated with the current thread, if + * any. + * @return the current FlowExecutionContext + * @throws IllegalStateException if no FlowExecutionContext is bound to this + * thread + */ + public static FlowExecutionContext getFlowExecutionContext() { + Assert.state(flowExecutionContextHolder.get() != null, + "No request context is bound to this thread"); + return (FlowExecutionContext) flowExecutionContextHolder.get(); + } + + // not instantiable + private FlowExecutionContextHolder() { + } +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolderUtils.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolderUtils.java index 0bff6d69..42416dc5 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolderUtils.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolderUtils.java @@ -20,6 +20,7 @@ import javax.faces.context.FacesContext; import javax.faces.el.EvaluationException; import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionContextHolder; /** * A static utility class for accessing the current flow execution holder. @@ -48,6 +49,7 @@ public class FlowExecutionHolderUtils { */ public static void setFlowExecutionHolder(FlowExecutionHolder holder, FacesContext context) { context.getExternalContext().getRequestMap().put(getFlowExecutionHolderKey(), holder); + FlowExecutionContextHolder.setFlowExecutionContext(holder.getFlowExecution()); } /** diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandler.java index 74f27d42..c3f52e27 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandler.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandler.java @@ -30,6 +30,7 @@ import org.springframework.webflow.definition.FlowDefinition; import org.springframework.webflow.definition.registry.FlowDefinitionLocator; import org.springframework.webflow.engine.NoMatchingTransitionException; import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionContextHolder; import org.springframework.webflow.execution.FlowExecutionFactory; import org.springframework.webflow.execution.ViewSelection; import org.springframework.webflow.executor.RequestParameterInputMapper; @@ -246,6 +247,7 @@ public class FlowNavigationHandler extends DecoratingNavigationHandler { if (logger.isDebugEnabled()) { logger.debug("Cleaning up allocated flow system resources"); } + FlowExecutionContextHolder.setFlowExecutionContext(null); FlowExecutionHolderUtils.unlockCurrentFlowExecutionIfNecessary(context); ExternalContextHolder.setExternalContext(null); } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java index 7e9ff9d4..8573ca2f 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java @@ -36,6 +36,7 @@ import org.springframework.webflow.core.collection.MutableAttributeMap; import org.springframework.webflow.definition.FlowDefinition; import org.springframework.webflow.definition.registry.FlowDefinitionLocator; import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionContextHolder; import org.springframework.webflow.execution.FlowExecutionFactory; import org.springframework.webflow.execution.ViewSelection; import org.springframework.webflow.execution.repository.FlowExecutionAccessException; @@ -481,6 +482,7 @@ public class FlowPhaseListener implements PhaseListener { if (logger.isDebugEnabled()) { logger.debug("Cleaning up allocated flow system resources"); } + FlowExecutionContextHolder.setFlowExecutionContext(null); FlowExecutionHolderUtils.unlockCurrentFlowExecutionIfNecessary(context); ExternalContextHolder.setExternalContext(null); } diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/scope/ConversationScopeTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/scope/ConversationScopeTests.java new file mode 100644 index 00000000..4e92ffbc --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/scope/ConversationScopeTests.java @@ -0,0 +1,105 @@ +/* + * 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.context.scope; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.FlowExecutionContextHolder; +import org.springframework.webflow.test.MockFlowExecutionContext; + +/** + * Test cases for the @{link ConversationScope} class. + * + * @author Ben Hale + */ +public class ConversationScopeTests extends TestCase { + + private MockFlowExecutionContext context; + + private ConversationScope scope; + + protected void setUp() { + context = new MockFlowExecutionContext(); + FlowExecutionContextHolder.setFlowExecutionContext(context); + scope = new ConversationScope(); + } + + protected void tearDown() { + scope = null; + context = null; + FlowExecutionContextHolder.setFlowExecutionContext(null); + } + + public void testGetVarMissing() { + StubObjectFactory factory = new StubObjectFactory(); + Object gotten = scope.get("name", factory); + assertNotNull("Should be real object", gotten); + assertTrue("Should have added object to the map", context.getConversationScope().contains("name")); + assertSame("Created object should have been returned", factory.getValue(), gotten); + assertSame("Created object should have been persisted", factory.getValue(), context.getConversationScope().get( + "name")); + } + + public void testGetVarExist() { + StubObjectFactory factory = new StubObjectFactory(); + Object value = new Object(); + context.getConversationScope().put("name", value); + Object gotten = scope.get("name", factory); + assertNotNull("Should be real object", gotten); + assertTrue("Should still be in map", context.getConversationScope().contains("name")); + assertSame("Persisted object should have been returned", value, gotten); + assertNotSame("Created object should not have been returned", factory.getValue(), gotten); + } + + public void testGetRequestContextMissing() { + FlowExecutionContextHolder.setFlowExecutionContext(null); + StubObjectFactory factory = new StubObjectFactory(); + try { + scope.get("name", factory); + fail("Should have thrown a ScopedBeanException without a request context"); + } catch (ScopedBeanException e) { + } + } + + public void testGetConversationId() { + String conversationId = scope.getConversationId(); + assertNull("Method not implemented yet, should return null", conversationId); + } + + public void testRemoveVarMissing() { + Object removed = scope.remove("name"); + assertFalse("Should have removed from object from map", context.getConversationScope().contains("name")); + assertNull("Should have returned a null object", removed); + } + + public void testRemoveVarExist() { + Object value = new Object(); + context.getConversationScope().put("name", value); + Object removed = scope.remove("name"); + assertFalse("Should have removed from object from map", context.getConversationScope().contains("name")); + assertSame("Should have returned the previous object", removed, value); + } + + public void testRemoveRequestContextMissing() { + FlowExecutionContextHolder.setFlowExecutionContext(null); + try { + scope.remove("name"); + fail("Should have thrown a ScopedBeanException without a request context"); + } catch (ScopedBeanException e) { + } + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/scope/FlashScopeTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/scope/FlashScopeTests.java new file mode 100644 index 00000000..cf4d6318 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/scope/FlashScopeTests.java @@ -0,0 +1,108 @@ +/* + * 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.context.scope; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.FlowExecutionContextHolder; +import org.springframework.webflow.test.MockFlowExecutionContext; + +/** + * Test cases for the + * @{link FlashScope} class. + * + * @author Ben Hale + */ +public class FlashScopeTests extends TestCase { + + private MockFlowExecutionContext context; + + private FlashScope scope; + + protected void setUp() { + context = new MockFlowExecutionContext(); + FlowExecutionContextHolder.setFlowExecutionContext(context); + scope = new FlashScope(); + } + + protected void tearDown() { + scope = null; + context = null; + FlowExecutionContextHolder.setFlowExecutionContext(null); + } + + public void testGetVarMissing() { + StubObjectFactory factory = new StubObjectFactory(); + Object gotten = scope.get("name", factory); + assertNotNull("Should be real object", gotten); + assertTrue("Should have added object to the map", context.getActiveSession().getFlashMap().contains("name")); + assertSame("Created object should have been returned", factory.getValue(), gotten); + assertSame("Created object should have been persisted", factory.getValue(), context.getActiveSession() + .getFlashMap().get("name")); + } + + public void testGetVarExist() { + StubObjectFactory factory = new StubObjectFactory(); + Object value = new Object(); + context.getActiveSession().getFlashMap().put("name", value); + Object gotten = scope.get("name", factory); + assertNotNull("Should be real object", gotten); + assertTrue("Should still be in map", context.getActiveSession().getFlashMap().contains("name")); + assertSame("Persisted object should have been returned", value, gotten); + assertNotSame("Created object should not have been returned", factory.getValue(), gotten); + } + + public void testGetRequestContextMissing() { + FlowExecutionContextHolder.setFlowExecutionContext(null); + StubObjectFactory factory = new StubObjectFactory(); + try { + scope.get("name", factory); + fail("Should have thrown a ScopedBeanException without a request context"); + } catch (ScopedBeanException e) { + } + } + + public void testGetConversationId() { + String flashId = scope.getConversationId(); + assertNull("Method not implemented yet, should return null", flashId); + } + + public void testRemoveVarMissing() { + Object removed = scope.remove("name"); + assertFalse("Should have removed from object from map", context.getActiveSession().getFlashMap().contains( + "name")); + assertNull("Should have returned a null object", removed); + } + + public void testRemoveVarExist() { + Object value = new Object(); + context.getActiveSession().getFlashMap().put("name", value); + Object removed = scope.remove("name"); + assertFalse("Should have removed from object from map", context.getActiveSession().getFlashMap().contains( + "name")); + assertSame("Should have returned the previous object", removed, value); + } + + public void testRemoveRequestContextMissing() { + FlowExecutionContextHolder.setFlowExecutionContext(null); + try { + scope.remove("name"); + fail("Should have thrown a ScopedBeanException without a request context"); + } catch (ScopedBeanException e) { + } + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/scope/FlowScopeTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/scope/FlowScopeTests.java new file mode 100644 index 00000000..77ceb8a1 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/scope/FlowScopeTests.java @@ -0,0 +1,106 @@ +/* + * 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.context.scope; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.FlowExecutionContextHolder; +import org.springframework.webflow.test.MockFlowExecutionContext; + +/** + * Test cases for the + * @{link FlowScope} class. + * + * @author Ben Hale + */ +public class FlowScopeTests extends TestCase { + + private MockFlowExecutionContext context; + + private FlowScope scope; + + protected void setUp() { + context = new MockFlowExecutionContext(); + FlowExecutionContextHolder.setFlowExecutionContext(context); + scope = new FlowScope(); + } + + protected void tearDown() { + scope = null; + context = null; + FlowExecutionContextHolder.setFlowExecutionContext(null); + } + + public void testGetVarMissing() { + StubObjectFactory factory = new StubObjectFactory(); + Object gotten = scope.get("name", factory); + assertNotNull("Should be real object", gotten); + assertTrue("Should have added object to the map", context.getActiveSession().getScope().contains("name")); + assertSame("Created object should have been returned", factory.getValue(), gotten); + assertSame("Created object should have been persisted", factory.getValue(), context.getActiveSession() + .getScope().get("name")); + } + + public void testGetVarExist() { + StubObjectFactory factory = new StubObjectFactory(); + Object value = new Object(); + context.getActiveSession().getScope().put("name", value); + Object gotten = scope.get("name", factory); + assertNotNull("Should be real object", gotten); + assertTrue("Should still be in map", context.getActiveSession().getScope().contains("name")); + assertSame("Persisted object should have been returned", value, gotten); + assertNotSame("Created object should not have been returned", factory.getValue(), gotten); + } + + public void testGetRequestContextMissing() { + FlowExecutionContextHolder.setFlowExecutionContext(null); + StubObjectFactory factory = new StubObjectFactory(); + try { + scope.get("name", factory); + fail("Should have thrown a ScopedBeanException without a request context"); + } catch (ScopedBeanException e) { + } + } + + public void testGetConversationId() { + String flowId = scope.getConversationId(); + assertNull("Method not implemented yet, should return null", flowId); + } + + public void testRemoveVarMissing() { + Object removed = scope.remove("name"); + assertFalse("Should have removed from object from map", context.getActiveSession().getScope().contains("name")); + assertNull("Should have returned a null object", removed); + } + + public void testRemoveVarExist() { + Object value = new Object(); + context.getActiveSession().getScope().put("name", value); + Object removed = scope.remove("name"); + assertFalse("Should have removed from object from map", context.getActiveSession().getScope().contains("name")); + assertSame("Should have returned the previous object", removed, value); + } + + public void testRemoveRequestContextMissing() { + FlowExecutionContextHolder.setFlowExecutionContext(null); + try { + scope.remove("name"); + fail("Should have thrown a ScopedBeanException without a request context"); + } catch (ScopedBeanException e) { + } + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/scope/StubObjectFactory.java b/spring-webflow/src/test/java/org/springframework/webflow/context/scope/StubObjectFactory.java new file mode 100644 index 00000000..e1926fe2 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/scope/StubObjectFactory.java @@ -0,0 +1,38 @@ +/* + * 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.context.scope; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectFactory; + +/** + * Stub implementation for testing the Spring Web Flow scopes. + * + * @author Ben Hale + */ +public class StubObjectFactory implements ObjectFactory { + + private Object value = new Object(); + + public Object getObject() throws BeansException { + return value; + } + + public Object getValue() { + return value; + } + +}