From 8a69dcc9f4b55432627ccbd562fc13f71b4dce58 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Fri, 13 Apr 2007 15:24:07 +0000 Subject: [PATCH] swf-290 --- .../executor/jsf/FlowExecutionHolder.java | 36 ++-- .../executor/jsf/FlowNavigationHandler.java | 183 +++++++++++++++--- ...lowNavigationHandlerArgumentExtractor.java | 107 ++++++++++ 3 files changed, 291 insertions(+), 35 deletions(-) create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandlerArgumentExtractor.java diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolder.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolder.java index f79bd1ae..814b5038 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolder.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolder.java @@ -24,24 +24,22 @@ import org.springframework.webflow.execution.repository.FlowExecutionKey; import org.springframework.webflow.execution.repository.FlowExecutionLock; /** - * A holder storing a reference to a flow execution and the key of that flow - * execution if it has been (or is about to be) managed in a repository. + * A holder storing a reference to a flow execution and the key of that flow execution if it has been (or is about to + * be) managed in a repository. * * @author Keith Donald */ public class FlowExecutionHolder implements Serializable { /** - * The flow execution continuation key (may be null if the flow execution - * has not yet been generated a repository key). May change as well over the - * life of this object, as a flow execution can be given a new key to - * capture its state at another point in time. + * The flow execution continuation key (may be null if the flow execution has not yet been generated a repository + * key). May change as well over the life of this object, as a flow execution can be given a new key to capture its + * state at another point in time. */ private FlowExecutionKey flowExecutionKey; /** - * The held flow execution representing the state of an ongoing conversation - * at a point in time. + * The held flow execution representing the state of an ongoing conversation at a point in time. */ private FlowExecution flowExecution; @@ -56,8 +54,7 @@ public class FlowExecutionHolder implements Serializable { private ViewSelection viewSelection; /** - * Creates a new flow execution holder for a flow execution that has not yet - * been placed in a repository. + * Creates a new flow execution holder for a flow execution that has not yet been placed in a repository. * @param flowExecution the flow execution to hold */ public FlowExecutionHolder(FlowExecution flowExecution) { @@ -65,8 +62,7 @@ public class FlowExecutionHolder implements Serializable { } /** - * Creates a new flow execution holder for a flow execution that has been - * restored from a repository. + * Creates a new flow execution holder for a flow execution that has been restored from a repository. * @param flowExecutionKey the continuation key * @param flowExecution the flow execution to hold * @param flowExecutionLock the lock acquired on the flow execution @@ -98,7 +94,7 @@ public class FlowExecutionHolder implements Serializable { public FlowExecution getFlowExecution() { return flowExecution; } - + /** * Returns the flow execution lock */ @@ -121,6 +117,20 @@ public class FlowExecutionHolder implements Serializable { this.viewSelection = viewSelection; } + /** + * Replace the current flow execution with the one provided. This method will clear out all state associated with + * the original execution and unlock it if necessary. + * @param flowExecution the new "current" flow execution + */ + public void replaceWith(FlowExecution flowExecution) { + this.flowExecutionKey = null; + this.viewSelection = null; + if (flowExecutionLock != null) { + flowExecutionLock.unlock(); + } + this.flowExecution = flowExecution; + } + public String toString() { return new ToStringCreator(this).append("flowExecutionKey", flowExecutionKey).append("flowExecution", flowExecution).toString(); 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 44233243..afc78fd4 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 @@ -18,13 +18,26 @@ package org.springframework.webflow.executor.jsf; import javax.faces.application.NavigationHandler; import javax.faces.context.FacesContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.web.jsf.DecoratingNavigationHandler; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.LocalAttributeMap; +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.FlowExecutionFactory; import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.executor.RequestParameterInputMapper; +import org.springframework.webflow.executor.support.FlowExecutorArgumentExtractor; /** * An implementation of a JSF NavigationHandler that provides integration with Spring Web Flow. - * Responsible for delegating to Spring Web Flow to resume flow executions by treating JSF action outcomes + * Responsible for delegating to Spring Web Flow to launch and resume flow executions, treating JSF action outcomes * (like a command button click) as web flow events. - *

+ * * This class delegates to the standard NavigationHandler implementation when a navigation request does not pertain to a * flow execution. *

@@ -33,43 +46,169 @@ import org.springframework.webflow.execution.ViewSelection; *

* If a flow execution has been restored in the current request: *

+ *

*

- * If a flow execution has not been restored in the current request simply delegate to the next NavigationHandler in - * the chain and return. + * If a flow execution has not been restored in the current request: + *

+ *

+ * How the flowId and eventId arguments are extracted can be customized by setting a custom + * {@link #setArgumentExtractor(FlowExecutorArgumentExtractor) argument extractor}. + * + * Note about customization: since NavigationHandlers managed directly by the JSF provider cannot be benefit from + * DependencyInjection, See Spring's {@link org.springframework.web.jsf.DelegatingNavigationHandlerProxy} when you need + * to customize a FlowNavigationHandler instance. * * @author Craig McClanahan * @author Colin Sampaleanu * @author Keith Donald */ -public class FlowNavigationHandler extends NavigationHandler { +public class FlowNavigationHandler extends DecoratingNavigationHandler { /** - * The standard navigation handler to delegate to when this one does not apply. + * Logger, usable by subclasses. */ - private NavigationHandler handlerDelegate; - + protected final Log logger = LogFactory.getLog(getClass()); + /** - * Create a new {@link FlowNavigationHandler} wrapping the specified standard navigation handler implementation. - * @param handlerDelegate the standard NavigationHandler this class decorates + * A helper for extracting parameters needed by this flow navigation handler. */ - public FlowNavigationHandler(NavigationHandler handlerDelegate) { - this.handlerDelegate = handlerDelegate; + private FlowExecutorArgumentExtractor argumentExtractor = new FlowNavigationHandlerArgumentExtractor(); + + /** + * The service responsible for mapping attributes of an {@link ExternalContext} to a new {@link FlowExecution} + * during the {@link #launch(String, ExternalContext) launch flow} operation. + *

+ * This allows developers to control what attributes are made available in the inputMap to new + * top-level flow executions. The starting execution may then choose to map that available input into its own local + * scope. + *

+ * The default implementation simply exposes all request parameters as flow execution input attributes. May be null. + */ + private AttributeMapper inputMapper = new RequestParameterInputMapper(); + + /** + * Create a new {@link FlowNavigationHandler} using the default constructor. + */ + public FlowNavigationHandler() { + super(); } - public void handleNavigation(FacesContext facesContext, String fromAction, String outcome) { - if (FlowExecutionHolderUtils.isFlowExecutionRestored(facesContext)) { - // a flow execution has been restored - signal an event against it - FlowExecutionHolder holder = FlowExecutionHolderUtils.getFlowExecutionHolder(facesContext); - JsfExternalContext context = new JsfExternalContext(facesContext, fromAction, outcome); - ViewSelection selectedView = holder.getFlowExecution().signalEvent(outcome, context); - holder.setViewSelection(selectedView); + /** + * Create a new {@link FlowNavigationHandler}, wrapping the specified standard navigation handler implementation. + * @param originalNavigationHandler Standard NavigationHandler we are wrapping + */ + public FlowNavigationHandler(NavigationHandler originalNavigationHandler) { + super(originalNavigationHandler); + } + + /** + * Returns the argument extractor used by this navigation handler. + */ + public FlowExecutorArgumentExtractor getArgumentExtractor() { + return argumentExtractor; + } + + /** + * Sets the argument extractor to use by this navigation handler. Call to customize how flow id and event id + * arguments are extracted. + */ + public void setArgumentExtractor(FlowExecutorArgumentExtractor argumentExtractor) { + this.argumentExtractor = argumentExtractor; + } + + /** + * Returns the configured flow execution input mapper. + */ + public AttributeMapper getInputMapper() { + return inputMapper; + } + + /** + * Sets the service responsible for mapping attributes of an {@link ExternalContext} to a new {@link FlowExecution} + * during a launch flow operation. + *

+ * The default implementation simply exposes all request parameters as flow execution input attributes. May be null. + * @see RequestParameterInputMapper + */ + public void setInputMapper(AttributeMapper inputMapper) { + this.inputMapper = inputMapper; + } + + public void handleNavigation(FacesContext facesContext, String fromAction, String outcome, + NavigationHandler originalNavigationHandler) { + JsfExternalContext context = new JsfExternalContext(facesContext, fromAction, outcome); + // first see if we need to launch a new flow execution if the flow id is present + if (argumentExtractor.isFlowIdPresent(context)) { + // a flow execution launch has been requested - create the new execution + String flowId = argumentExtractor.extractFlowId(context); + FlowDefinition flowDefinition = getLocator(context).getFlowDefinition(flowId); + FlowExecution flowExecution = getFactory(context).createFlowExecution(flowDefinition); + // check to see if this execution was created while another was running + if (FlowExecutionHolderUtils.isFlowExecutionRestored(facesContext)) { + // replace the current flow execution with the new one + FlowExecutionHolderUtils.getFlowExecutionHolder(facesContext).replaceWith(flowExecution); + } else { + // bind the new execution as the 'current execution' + FlowExecutionHolder holder = new FlowExecutionHolder(flowExecution); + FlowExecutionHolderUtils.setFlowExecutionHolder(holder, facesContext); + } + // start the new execution + ViewSelection selectedView = flowExecution.start(createInput(context), context); + // set the starting view to render + FlowExecutionHolderUtils.getFlowExecutionHolder(facesContext).setViewSelection(selectedView); + } else { + // not a launch request - see if this is a resume request to continue an existing execution + if (FlowExecutionHolderUtils.isFlowExecutionRestored(facesContext)) { + // a flow execution has been restored - see if we need to signal an event against it + if (argumentExtractor.isEventIdPresent(context)) { + // signal the event against the current flow execution + String eventId = argumentExtractor.extractEventId(context); + FlowExecutionHolder holder = FlowExecutionHolderUtils.getFlowExecutionHolder(facesContext); + ViewSelection selectedView = holder.getFlowExecution().signalEvent(eventId, context); + // set the next view to render + holder.setViewSelection(selectedView); + } + } + else { + // neither a flow launch or resume request: proceed with standard navigation + originalNavigationHandler.handleNavigation(facesContext, fromAction, outcome); + } + } + } + + /** + * Factory method that creates the input attribute map for a newly created {@link FlowExecution}. This + * implementation uses the registered input mapper, if any. + * @param context the external context + * @return the input map, or null if no input + */ + protected MutableAttributeMap createInput(ExternalContext context) { + if (inputMapper != null) { + MutableAttributeMap inputMap = new LocalAttributeMap(); + inputMapper.map(context, inputMap, null); + return inputMap; } else { - // no flow execution is restored, proceed with standard navigation - handlerDelegate.handleNavigation(facesContext, fromAction, outcome); + return null; } } + + // helpers + + private FlowDefinitionLocator getLocator(JsfExternalContext context) { + return FlowFacesUtils.getDefinitionLocator(context.getFacesContext()); + } + + private FlowExecutionFactory getFactory(JsfExternalContext context) { + return FlowFacesUtils.getExecutionFactory(context.getFacesContext()); + } } \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandlerArgumentExtractor.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandlerArgumentExtractor.java new file mode 100644 index 00000000..d312a3ae --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandlerArgumentExtractor.java @@ -0,0 +1,107 @@ +/* + * 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 org.springframework.util.StringUtils; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.executor.support.FlowExecutorArgumentExtractionException; +import org.springframework.webflow.executor.support.FlowExecutorArgumentExtractor; + +/** + * An {@link FlowExecutorArgumentExtractor} that is aware of JSF outcomes that communicate requests to launch flow + * executions and signal event in existing flow executions. Designed to be used wih a {@link FlowNavigationHandler}. + * + * Note: this class only implements flow id and event id extraction methods. A FlowNavigationHandler is not expected to + * extract a flow execution key, as flow execution restoration is fully handled by the {@link FlowPhaseListener} and the + * JSF restore view phase. + * + * @author Keith Donald + */ +public class FlowNavigationHandlerArgumentExtractor implements FlowExecutorArgumentExtractor { + + /** + * The default prefix of a JSF outcome string that indicates a new flow should be launched. + */ + private static final String FLOW_ID_PREFIX = "flowId:"; + + /** + * The prefix for JSF outcome strings indicating a new flow should be launched. + */ + private String flowIdPrefix = FLOW_ID_PREFIX; + + /** + * Returns the configured prefix for outcome strings that indicate a new flow should be launched. + */ + public String getFlowIdPrefix() { + return flowIdPrefix; + } + + /** + * Sets the prefix of an outcome string that indicates a new flow should be launched. + */ + public void setFlowIdPrefix(String flowIdPrefix) { + this.flowIdPrefix = flowIdPrefix; + } + + public boolean isFlowIdPresent(ExternalContext context) throws FlowExecutorArgumentExtractionException { + String outcome = getOutcome(context); + if (outcome != null && outcome.startsWith(getFlowIdPrefix())) { + return true; + } + else { + return false; + } + } + + public String extractFlowId(ExternalContext context) throws FlowExecutorArgumentExtractionException { + // extract the flowId from a JSF outcome in format ${flowIdPrefix}${flowId} + String outcome = getOutcome(context); + int index = outcome.indexOf(getFlowIdPrefix()); + if (index == -1) { + throw new FlowExecutorArgumentExtractionException( + "Unable to extract flow id; make sure the JSF outcome is prefixed with '" + getFlowIdPrefix() + + "' to launch a new flow execution"); + } + String flowId = outcome.substring(getFlowIdPrefix().length()); + if (!StringUtils.hasText(flowId)) { + throw new FlowExecutorArgumentExtractionException( + "Unable to extract flow id; make sure the flow id is provided in the outcome string"); + } + return flowId; + } + + public boolean isEventIdPresent(ExternalContext context) { + return StringUtils.hasText(getOutcome(context)); + } + + public String extractEventId(ExternalContext context) throws FlowExecutorArgumentExtractionException { + // treat the action outcome string as the event id + return getOutcome(context); + } + + public boolean isFlowExecutionKeyPresent(ExternalContext context) { + throw new UnsupportedOperationException("Should not be called by a FlowNavigationHandler"); + } + + public String extractFlowExecutionKey(ExternalContext context) throws FlowExecutorArgumentExtractionException { + throw new UnsupportedOperationException("Should not be called by a FlowNavigationHandler"); + } + + // helpers + private String getOutcome(ExternalContext context) { + return ((JsfExternalContext) context).getOutcome(); + } +} \ No newline at end of file