automatic exposing of flow execution key in view root, so no need to post back manually now in jsf env

This commit is contained in:
Keith Donald
2007-04-02 19:51:01 +00:00
parent 4645cff0b7
commit 179cb79a58
3 changed files with 206 additions and 98 deletions

View File

@@ -165,7 +165,7 @@
<sect2 id="executor-custom-repo">
<title>A flow executor using a simple execution repository</title>
<programlisting>
&lt;flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="simple"/>
&lt;flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="simple"/&gt;
</programlisting>
<para>
This executor is configured with a simple repository that manages
@@ -175,7 +175,7 @@
<sect2 id="executor-custom-repo2">
<title>A flow executor using a client-side continuation-based execution repository</title>
<programlisting>
&lt;flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="client"/>
&lt;flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="client"/&gt;
</programlisting>
<para>
This executor is configured with a continuation-based repository that serializes
@@ -185,7 +185,7 @@
<sect2 id="executor-custom-repo3">
<title>A flow executor using a single key execution repository</title>
<programlisting>
&lt;flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="singleKey"/>
&lt;flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="singleKey"/>&gt;
</programlisting>
<para>
This executor is configured with a simple repository that assigns a single
@@ -193,6 +193,26 @@
for the duration of the conversation.
</para>
</sect2>
<sect2 id="executor-custom-repo4">
<title>A flow executor setting custom conversation management attributes</title>
<programlisting>
&lt;flow:executor id="flowExecutor" registry-ref="flowRegistry">
&lt;flow:repository type="continuation" max-conversations="5" max-continuations="30" conversation-manager-ref="conversationManager"/&gt;
&lt;/flow:executor&gt;
&lt;bean id="conversationManager" class="example.MyCustomConversationalStateManager"/&gt;
</programlisting>
<para>
This executor is configured with a continuation repository configured with custom settings for:
<orderedlist>
<listitem><para>The maximum number of active conversations per user session (5)</para></listitem>
<listitem><para>The maximum number of restorable flow execution snapshots (continuations) per conversation (30)</para></listitem>
<listitem><para>Where conversational state will be stored (via a custom conversationManager)</para></listitem>
</orderedlist>
The <literal>flow:repository</literal> child element is the more flexible form for configuring the flow execution repository.
Use it or the convenient <literal>repository-type</literal> attribute, not both.
</para>
</sect2>
<sect2 id="executor-excution-attributes">
<title>A flow executor setting system execution attributes</title>
<programlisting>
@@ -227,7 +247,7 @@
&lt;/flow-executor&gt;
&lt;!-- A FlowExecutionListener to observe the lifecycle of order-flow executions --&gt;
&lt;bean id="listener" class="org.springframework.webflow.samples.sellitem.SellItemFlowExecutionListener"/&gt;
&lt;bean id="listener" class="example.OrderFlowExecutionListener"/&gt;
</programlisting>
<para>
This executor is configured to apply the execution listener to the "order-flow".
@@ -503,7 +523,8 @@
<para>
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.
</para>
<sect2 id="executor-jsf-simple">
<title>A typical faces-config.xml file</title>
@@ -513,18 +534,61 @@
&lt;navigation-handler&gt;
org.springframework.webflow.executor.jsf.FlowNavigationHandler
&lt;/navigation-handler&gt;
&lt;variable-resolver&gt;
org.springframework.webflow.executor.jsf.FlowExecutionVariableResolver
&lt;/variable-resolver&gt;
&lt;property-resolver&gt;
org.springframework.webflow.executor.jsf.FlowPropertyResolver
org.springframework.webflow.executor.jsf.FlowExecutionPropertyResolver
&lt;/property-resolver&gt;
&lt;/application&gt;
&lt;lifecycle&gt;
&lt;phase-listener&gt;org.springframework.webflow.executor.jsf.FlowPhaseListener&lt;/phase-listener&gt;
&lt;/lifecycle&gt;
&lt;/faces-config&gt;
</programlisting>
</sect>
<sect2 id="executor-jsf-launch-get">
<title>Launching a flow execution - command link</title>
<programlisting>
&lt;h:commandLink value="Go" action="flowId:myflow"/&gt;
</programlisting>
</sect2>
<sect2 id="executor-jsf-resume-form">
<title>Resuming a flow execution - form bound to flow execution variables</title>
<programlisting>
&lt;h:form id="form"&gt;
...
&lt;h:inputText id="propertyName" value="#{flowExecution.flashScope.aFlashScopeAttribute}"/&gt;
&lt;h:inputText id="propertyName" value="#{flowExecution.flowScope.aFlowScopeAttribute}"/&gt;
&lt;h:inputText id="propertyName" value="#{flowExecution.conversationScope.aConversationScopeAttribute}"/&gt;
&lt;h:inputText id="propertyName" value="#{flowExecution.anAttributeToSearchForAcrossAllScopes}"/&gt;
...
&lt;input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"&gt;
&lt;h:commandButton type="submit" value="Next" action="submit"/&gt;
&lt;/h:form&gt;
</programlisting>
</sect2>
<sect2 id="executor-jsf-simple">
<title>A pre Spring Web Flow 1.0.2 faces-config.xml file</title>
<para>
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:
</para>
<programlisting>
&lt;faces-config&gt;
&lt;application&gt;
&lt;navigation-handler&gt;
org.springframework.webflow.executor.jsf.FlowNavigationHandler
&lt;/navigation-handler&gt;
&lt;variable-resolver&gt;
org.springframework.webflow.executor.jsf.FlowVariableResolver
&lt;/variable-resolver&gt;
&lt;variable-resolver&gt;
org.springframework.web.jsf.DelegatingVariableResolver
&lt;/variable-resolver&gt;
&lt;variable-resolver>
org.springframework.web.jsf.WebApplicationContextVariableResolver
&lt;/variable-resolver&gt;
&lt;property-resolver&gt;
org.springframework.webflow.executor.jsf.FlowPropertyResolver
&lt;/property-resolver&gt;
&lt;/application&gt;
&lt;lifecycle&gt;
@@ -533,18 +597,12 @@
&lt;/faces-config&gt;
</programlisting>
</sect2>
<sect2 id="executor-jsf-launch-get">
<title>Launching a flow execution - command link</title>
<programlisting>
&lt;h:commandLink value="Go" action="flowId:myflow"/&gt;
</programlisting>
</sect2>
<sect2 id="executor-jsf-resume-form">
<title>Resuming a flow execution - form</title>
<title>Resuming a flow execution - pre 1.0.2 form bound to flow scope variables</title>
<programlisting>
&lt;h:form id="form"&gt;
...
&lt;h:inputText id="propertyName" value="#{flowScope.managedBeanName.propertyName}"/&gt;
&lt;h:inputText id="propertyName" value="#{flowScope.aFlowScopeAttribute}"/&gt;
...
&lt;input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"&gt;
&lt;h:commandButton type="submit" value="Next" action="submit"/&gt;

View File

@@ -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.
* <p> This phase listener implements the following algorithm: <ul> <li>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. <li>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. <li>On
* AFTER_RENDER_RESPONSE, if a flow execution was restored in the RESTORE_VIEW
* phase <em>save</em> the updated execution to the repository using the new
* key generated in the BEFORE_RENDER_RESPONSE phase. </ul>
* 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.
* <p>
* This phase listener implements the following algorithm:
* <ul>
* <li>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.
* <li>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.
* <li>On AFTER_RENDER_RESPONSE, if a flow execution was restored in the RESTORE_VIEW phase <em>save</em> the updated
* execution to the repository using the new key generated in the BEFORE_RENDER_RESPONSE phase.
* </ul>
*
* @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.
* <p>
* This allows developers to control what attributes are made available in
* the <code>inputMap</code> 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 <code>inputMap</code> to new
* top-level flow executions. The starting execution may then choose to map that available input into its own local
* scope.
* <p>
* 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.
* <p>
* 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) {

View File

@@ -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);
}
}
}