diff --git a/spring-webflow/docs/reference/src/flow-executor.xml b/spring-webflow/docs/reference/src/flow-executor.xml index b0338035..88dd02d2 100644 --- a/spring-webflow/docs/reference/src/flow-executor.xml +++ b/spring-webflow/docs/reference/src/flow-executor.xml @@ -165,7 +165,7 @@ A flow executor using a simple execution repository - <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="simple"/> + <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="simple"/> This executor is configured with a simple repository that manages @@ -175,7 +175,7 @@ A flow executor using a client-side continuation-based execution repository - <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="client"/> + <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="client"/> This executor is configured with a continuation-based repository that serializes @@ -185,7 +185,7 @@ A flow executor using a single key execution repository - <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="singleKey"/> + <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="singleKey"/>> This executor is configured with a simple repository that assigns a single @@ -193,6 +193,26 @@ for the duration of the conversation. + + A flow executor setting custom conversation management attributes + + <flow:executor id="flowExecutor" registry-ref="flowRegistry"> + <flow:repository type="continuation" max-conversations="5" max-continuations="30" conversation-manager-ref="conversationManager"/> + </flow:executor> + + <bean id="conversationManager" class="example.MyCustomConversationalStateManager"/> + + + This executor is configured with a continuation repository configured with custom settings for: + + The maximum number of active conversations per user session (5) + The maximum number of restorable flow execution snapshots (continuations) per conversation (30) + Where conversational state will be stored (via a custom conversationManager) + + The flow:repository child element is the more flexible form for configuring the flow execution repository. + Use it or the convenient repository-type attribute, not both. + + A flow executor setting system execution attributes @@ -227,7 +247,7 @@ </flow-executor> <!-- A FlowExecutionListener to observe the lifecycle of order-flow executions --> - <bean id="listener" class="org.springframework.webflow.samples.sellitem.SellItemFlowExecutionListener"/> + <bean id="listener" class="example.OrderFlowExecutionListener"/> This executor is configured to apply the execution listener to the "order-flow". @@ -503,7 +523,8 @@ Spring Web Flow integrates with JSF. The JSF integration relies on custom implementations of core JSF artifacts such as navigation handler and phase listener to drive the - execution of flows. + execution of flows. In addition, it relies on custom Variable and Property Resolvers to + access flow execution variables from JSF components. A typical faces-config.xml file @@ -513,18 +534,61 @@ <navigation-handler> org.springframework.webflow.executor.jsf.FlowNavigationHandler </navigation-handler> + <variable-resolver> + org.springframework.webflow.executor.jsf.FlowExecutionVariableResolver + </variable-resolver> <property-resolver> - org.springframework.webflow.executor.jsf.FlowPropertyResolver + org.springframework.webflow.executor.jsf.FlowExecutionPropertyResolver </property-resolver> + </application> + + <lifecycle> + <phase-listener>org.springframework.webflow.executor.jsf.FlowPhaseListener</phase-listener> + </lifecycle> +</faces-config> + + + + Launching a flow execution - command link + + <h:commandLink value="Go" action="flowId:myflow"/> + + + + Resuming a flow execution - form bound to flow execution variables + + <h:form id="form"> + ... + <h:inputText id="propertyName" value="#{flowExecution.flashScope.aFlashScopeAttribute}"/> + <h:inputText id="propertyName" value="#{flowExecution.flowScope.aFlowScopeAttribute}"/> + <h:inputText id="propertyName" value="#{flowExecution.conversationScope.aConversationScopeAttribute}"/> + + <h:inputText id="propertyName" value="#{flowExecution.anAttributeToSearchForAcrossAllScopes}"/> + ... + <input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"> + <h:commandButton type="submit" value="Next" action="submit"/> + </h:form> + + + + A pre Spring Web Flow 1.0.2 faces-config.xml file + + Before Spring Web Flow 1.0.2 Spring Web Flow only supported resolving variables in flow scope + (and not the other scopes such as flash and conversation shown above). This configuration is still supported + for backwards compatibility reasons and follows: + + +<faces-config> + <application> + <navigation-handler> + org.springframework.webflow.executor.jsf.FlowNavigationHandler + </navigation-handler> <variable-resolver> org.springframework.webflow.executor.jsf.FlowVariableResolver </variable-resolver> - <variable-resolver> - org.springframework.web.jsf.DelegatingVariableResolver - </variable-resolver> - <variable-resolver> - org.springframework.web.jsf.WebApplicationContextVariableResolver - </variable-resolver> + <property-resolver> + org.springframework.webflow.executor.jsf.FlowPropertyResolver + </property-resolver> </application> <lifecycle> @@ -533,18 +597,12 @@ </faces-config> - - Launching a flow execution - command link - - <h:commandLink value="Go" action="flowId:myflow"/> - - - Resuming a flow execution - form + Resuming a flow execution - pre 1.0.2 form bound to flow scope variables <h:form id="form"> ... - <h:inputText id="propertyName" value="#{flowScope.managedBeanName.propertyName}"/> + <h:inputText id="propertyName" value="#{flowScope.aFlowScopeAttribute}"/> ... <input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"> <h:commandButton type="submit" value="Next" action="submit"/> 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 4d9abd5e..ee817682 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 @@ -52,23 +52,21 @@ import org.springframework.webflow.executor.support.RequestParameterFlowExecutor import org.springframework.webflow.executor.support.ResponseInstructionHandler; /** - * JSF phase listener that is responsible for managing a {@link FlowExecution} - * object representing an active user conversation so that other JSF artifacts - * that execute in different phases of the JSF lifecycle may have access to it. - *

This phase listener implements the following algorithm:

  • On - * BEFORE_RESTORE_VIEW, restore the {@link FlowExecution} the user is - * participating in if a call to - * {@link FlowExecutorArgumentHandler#extractFlowExecutionKey(ExternalContext)} - * returns a submitted flow execution identifier. Place the restored flow - * execution in a holder that other JSF artifacts such as VariableResolvers, - * PropertyResolvers, and NavigationHandlers may access during the request - * lifecycle.
  • On BEFORE_RENDER_RESPONSE, if a flow execution was restored - * in the RESTORE_VIEW phase generate a new key for identifying the updated - * execution within a the selected {@link FlowExecutionRepository}. Expose - * managed flow execution attributes to the views before rendering.
  • On - * AFTER_RENDER_RESPONSE, if a flow execution was restored in the RESTORE_VIEW - * phase save the updated execution to the repository using the new - * key generated in the BEFORE_RENDER_RESPONSE phase.
+ * JSF phase listener that is responsible for managing a {@link FlowExecution} object representing an active user + * conversation so that other JSF artifacts that execute in different phases of the JSF lifecycle may have access to it. + *

+ * This phase listener implements the following algorithm: + *

    + *
  • On BEFORE_RESTORE_VIEW, restore the {@link FlowExecution} the user is participating in if a call to + * {@link FlowExecutorArgumentHandler#extractFlowExecutionKey(ExternalContext)} returns a submitted flow execution + * identifier. Place the restored flow execution in a holder that other JSF artifacts such as VariableResolvers, + * PropertyResolvers, and NavigationHandlers may access during the request lifecycle. + *
  • On BEFORE_RENDER_RESPONSE, if a flow execution was restored in the RESTORE_VIEW phase generate a new key for + * identifying the updated execution within a the selected {@link FlowExecutionRepository}. Expose managed flow + * execution attributes to the views before rendering. + *
  • On AFTER_RENDER_RESPONSE, if a flow execution was restored in the RESTORE_VIEW phase save the updated + * execution to the repository using the new key generated in the BEFORE_RENDER_RESPONSE phase. + *
* * @author Colin Sampaleanu * @author Keith Donald @@ -79,27 +77,24 @@ public class FlowPhaseListener implements PhaseListener { * Logger, usable by subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - + /** * A helper for handling arguments needed by this phase listener to resume and launch flow executions. */ private FlowExecutorArgumentHandler argumentHandler = new RequestParameterFlowExecutorArgumentHandler(); /** - * The service responsible for mapping attributes of an - * {@link ExternalContext} to a new {@link FlowExecution} during the - * {@link #launch(String, ExternalContext) launch flow} operation. + * 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. + * 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. + * The default implementation simply exposes all request parameters as flow execution input attributes. May be null. */ private AttributeMapper inputMapper = new RequestParameterInputMapper(); - + /** * Resolves selected Web Flow view names to JSF view ids. */ @@ -127,17 +122,16 @@ public class FlowPhaseListener implements PhaseListener { } /** - * Sets the service responsible for mapping attributes of an - * {@link ExternalContext} to a new {@link FlowExecution} during a launch flow operation. + * 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. + * 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; } - + /** * Returns the JSF view id resolver used by this phase listener. */ @@ -157,11 +151,7 @@ public class FlowPhaseListener implements PhaseListener { } public void beforePhase(PhaseEvent event) { - if (event.getPhaseId() == PhaseId.RESTORE_VIEW) { - ExternalContextHolder.setExternalContext(new JsfExternalContext(event.getFacesContext())); - restoreFlowExecution(event.getFacesContext()); - } - else if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { + if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { if (FlowExecutionHolderUtils.isFlowExecutionRestored(event.getFacesContext())) { prepareResponse(getCurrentContext(), FlowExecutionHolderUtils.getFlowExecutionHolder(event .getFacesContext())); @@ -170,7 +160,10 @@ public class FlowPhaseListener implements PhaseListener { } public void afterPhase(PhaseEvent event) { - if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { + if (event.getPhaseId() == PhaseId.RESTORE_VIEW) { + ExternalContextHolder.setExternalContext(new JsfExternalContext(event.getFacesContext())); + restoreFlowExecution(event.getFacesContext()); + } else if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { try { if (FlowExecutionHolderUtils.isFlowExecutionRestored(event.getFacesContext())) { FlowExecutionHolder holder = FlowExecutionHolderUtils.getFlowExecutionHolder(event @@ -191,20 +184,25 @@ public class FlowPhaseListener implements PhaseListener { } } - private JsfExternalContext getCurrentContext() { - return (JsfExternalContext) ExternalContextHolder.getExternalContext(); - } - protected void restoreFlowExecution(FacesContext facesContext) { JsfExternalContext context = new JsfExternalContext(facesContext); - if (argumentHandler.isFlowExecutionKeyPresent(context)) { + if (argumentHandler.isFlowExecutionKeyPresent(context) || isFlowExecutionKeyInViewRoot(facesContext)) { // restore flow execution from repository so it will be // available to variable/property resolvers and the flow // navigation handler (this could happen as part of a submission or // flow execution redirect) FlowExecutionRepository repository = getRepository(context); - FlowExecutionKey flowExecutionKey = repository.parseFlowExecutionKey(argumentHandler - .extractFlowExecutionKey(context)); + FlowExecutionKey flowExecutionKey; + if (argumentHandler.isFlowExecutionKeyPresent(context)) { + // extract it in the "traditional way" (request parameter in url by default) + flowExecutionKey = repository.parseFlowExecutionKey(argumentHandler.extractFlowExecutionKey(context)); + } + else { + // restore the key from an attribute in the root of the component tree + flowExecutionKey = repository.parseFlowExecutionKey((String)facesContext.getViewRoot().getAttributes().get("_flowExecutionKey")); + // remove it (it should always be placed back before response rendering) + facesContext.getViewRoot().getAttributes().remove("_flowExecutionKey"); + } FlowExecutionLock lock = repository.getLock(flowExecutionKey); lock.lock(); FlowExecution flowExecution = repository.getFlowExecution(flowExecutionKey); @@ -231,9 +229,8 @@ public class FlowPhaseListener implements PhaseListener { } /** - * Factory method that creates the input attribute map for a newly created - * {@link FlowExecution}. This implementation uses the registered input mapper, - * if any. + * 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 */ @@ -295,9 +292,74 @@ public class FlowPhaseListener implements PhaseListener { } Map requestMap = facesContext.getExternalContext().getRequestMap(); String flowExecutionKey = holder.getFlowExecution().isActive() ? holder.getFlowExecutionKey().toString() : null; + if (flowExecutionKey != null) { + // expose to view root for preservation in the component tree + if (viewRootAttributeMapPresent(facesContext)) { + facesContext.getViewRoot().getAttributes().put("_flowExecutionKey", flowExecutionKey); + } + } argumentHandler.exposeFlowExecutionContext(flowExecutionKey, holder.getFlowExecution(), requestMap); } + /** + * Updates the current flow execution in the repository. + * @param context the external context + * @param holder the current flow execution holder + */ + protected void saveFlowExecution(JsfExternalContext context, FlowExecutionHolder holder) { + FlowExecution flowExecution = holder.getFlowExecution(); + FlowExecutionRepository repository = getRepository(context); + if (flowExecution.isActive()) { + // save the flow execution out to the repository + if (logger.isDebugEnabled()) { + logger.debug("Saving continuation to repository with key " + holder.getFlowExecutionKey()); + } + repository.putFlowExecution(holder.getFlowExecutionKey(), flowExecution); + } + else { + if (holder.getFlowExecutionKey() != null) { + // remove the flow execution from the repository + if (logger.isDebugEnabled()) { + logger.debug("Removing execution in repository with key '" + holder.getFlowExecutionKey() + "'"); + } + repository.removeFlowExecution(holder.getFlowExecutionKey()); + } + } + } + + // private helpers + + private JsfExternalContext getCurrentContext() { + return (JsfExternalContext) ExternalContextHolder.getExternalContext(); + } + + /** + * Returns true if the root of the component tree contains the flow execution key attribute, used + * to restore the flow execution on subsequent reqests. + * @param facesContext the key + * @return true if yes, false otherwise + */ + private boolean isFlowExecutionKeyInViewRoot(FacesContext facesContext) { + if (viewRootAttributeMapPresent(facesContext)) { + return facesContext.getViewRoot().getAttributes().containsKey("_flowExecutionKey"); + } else { + return false; + } + } + + /** + * Simple little helper that returns true if the view root attribute map is non-null. + * @param facesContext the faces context + * @return true if so, false otherwise + */ + private boolean viewRootAttributeMapPresent(FacesContext facesContext) { + if (facesContext.getViewRoot() != null && facesContext.getViewRoot().getAttributes() != null) { + return true; + } else { + return false; + } + } + private void updateViewRoot(FacesContext facesContext, String viewId) { UIViewRoot viewRoot = facesContext.getViewRoot(); if (viewRoot == null || hasViewChanged(viewRoot, viewId)) { @@ -332,31 +394,9 @@ public class FlowPhaseListener implements PhaseListener { } } - protected void saveFlowExecution(JsfExternalContext context, FlowExecutionHolder holder) { - FlowExecution flowExecution = holder.getFlowExecution(); - FlowExecutionRepository repository = getRepository(context); - if (flowExecution.isActive()) { - // save the flow execution out to the repository - if (logger.isDebugEnabled()) { - logger.debug("Saving continuation to repository with key " + holder.getFlowExecutionKey()); - } - repository.putFlowExecution(holder.getFlowExecutionKey(), flowExecution); - } - else { - if (holder.getFlowExecutionKey() != null) { - // remove the flow execution from the repository - if (logger.isDebugEnabled()) { - logger.debug("Removing execution in repository with key '" + holder.getFlowExecutionKey() + "'"); - } - repository.removeFlowExecution(holder.getFlowExecutionKey()); - } - } - } - /** - * Utility method needed needed only because we can not rely on JSF - * RequestMap supporting Map's putAll method. Tries putAll, falls back to - * individual adds + * Utility method needed needed only because we can not rely on JSF RequestMap supporting Map's putAll method. Tries + * putAll, falls back to individual adds * @param targetMap the target map to add the model data to * @param map the model data to add to the target map */ @@ -387,8 +427,7 @@ public class FlowPhaseListener implements PhaseListener { } /** - * Standard default view id resolver which uses the web flow view name as - * the jsf view id + * Standard default view id resolver which uses the web flow view name as the jsf view id */ public static class DefaultViewIdMapper implements ViewIdMapper { public String mapViewId(String viewName) { diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/ResponseInstructionHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/ResponseInstructionHandler.java index d4a7620f..c24346ee 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/ResponseInstructionHandler.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/ResponseInstructionHandler.java @@ -15,6 +15,7 @@ */ package org.springframework.webflow.executor.support; +import org.springframework.core.NestedRuntimeException; import org.springframework.webflow.execution.ViewSelection; import org.springframework.webflow.execution.support.ApplicationView; import org.springframework.webflow.execution.support.ExternalRedirect; @@ -99,8 +100,8 @@ public abstract class ResponseInstructionHandler { return handle(responseInstruction); } catch (Exception e) { - throw new RuntimeException( - "Unexpected exception handling response instruction " + responseInstruction + ": " + e); + throw new RuntimeResponseHandlingException( + "Unexpected exception handling response instruction " + responseInstruction + "", e); } } @@ -149,4 +150,14 @@ public abstract class ResponseInstructionHandler { * @see ViewSelection#NULL_VIEW */ protected abstract void handleNull() throws Exception; -} + + /** + * Thrown during handleQuietly. + * @author Keith Donald + */ + public static class RuntimeResponseHandlingException extends NestedRuntimeException { + public RuntimeResponseHandlingException(String message, Throwable cause) { + super(message, cause); + } + } +} \ No newline at end of file