SWF 305
This commit is contained in:
@@ -18,6 +18,8 @@ Package org.springframework.webflow.executor
|
||||
RENDER RESPONSE phase is bypassed (SWF-302).
|
||||
* Restored compatability with 1.0.1 allowing default navigation rules to be queried if no transition could
|
||||
be matched against the state of the current flow execution (SWF-303).
|
||||
* JSF integration code now tears down ExternalContext thread local properly in exceptional situations and when
|
||||
the RENDER_RESPONSE phase is bypassed (SWF-305).
|
||||
|
||||
Changes in version 1.0.3 (19.04.2007)
|
||||
-------------------------------------
|
||||
|
||||
@@ -23,6 +23,7 @@ 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.context.ExternalContextHolder;
|
||||
import org.springframework.webflow.core.collection.LocalAttributeMap;
|
||||
import org.springframework.webflow.core.collection.MutableAttributeMap;
|
||||
import org.springframework.webflow.definition.FlowDefinition;
|
||||
@@ -35,33 +36,38 @@ import org.springframework.webflow.executor.RequestParameterInputMapper;
|
||||
import org.springframework.webflow.executor.support.FlowExecutorArgumentExtractor;
|
||||
|
||||
/**
|
||||
* An implementation of a JSF <code>NavigationHandler</code> that provides
|
||||
* integration with Spring Web Flow. 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.
|
||||
* An implementation of a JSF <code>NavigationHandler</code> that provides integration with Spring Web Flow.
|
||||
* 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. <p> The following
|
||||
* navigation handler algorithm is implemented by default: </p> <p> If a flow
|
||||
* execution has been restored in the current request: <ul> <li>Resume the flow
|
||||
* execution by signaling the JSF action outcome as an event against the current
|
||||
* state. <li>Once event processing completes expose the selected view as the
|
||||
* "current" {@link ViewSelection}. </ul> </p> <p> If a flow execution has not
|
||||
* been restored in the current request: <ul> <li>If the specified logical
|
||||
* outcome is of the form <em>flowId:xxx</em> look up the corresponding
|
||||
* {@link FlowDefinition} with that id and launch a new flow execution in the
|
||||
* starting state. Expose the new execution as the "current" flow execution for
|
||||
* this request. Expose the first selected view as the "current" view selection.
|
||||
* <li>If the specified logical outcome is not of the form <em>flowId:xxx</em>,
|
||||
* simply delegate to the standard <code>NavigationHandler</code>
|
||||
* implementation and return. </ul> </p> How the flowId and eventId arguments
|
||||
* are extracted can be customized by setting a custom
|
||||
* This class delegates to the standard NavigationHandler implementation when a navigation request does not pertain to a
|
||||
* flow execution.
|
||||
* <p>
|
||||
* The following navigation handler algorithm is implemented by default:
|
||||
* </p>
|
||||
* <p>
|
||||
* If a flow execution has been restored in the current request:
|
||||
* <ul>
|
||||
* <li>Resume the flow execution by signaling the JSF action outcome as an event against the current state.
|
||||
* <li>Once event processing completes expose the selected view as the "current" {@link ViewSelection}.
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>
|
||||
* If a flow execution has not been restored in the current request:
|
||||
* <ul>
|
||||
* <li>If the specified logical outcome is of the form <em>flowId:xxx</em> look up the corresponding
|
||||
* {@link FlowDefinition} with that id and launch a new flow execution in the starting state. Expose the new execution
|
||||
* as the "current" flow execution for this request. Expose the first selected view as the "current" view selection.
|
||||
* <li>If the specified logical outcome is not of the form <em>flowId:xxx</em>, simply delegate to the standard
|
||||
* <code>NavigationHandler</code> implementation and return.
|
||||
* </ul>
|
||||
* </p>
|
||||
* 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.
|
||||
* 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
|
||||
@@ -75,20 +81,19 @@ public class FlowNavigationHandler extends DecoratingNavigationHandler {
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
/**
|
||||
* A helper for extracting parameters needed by this flow navigation
|
||||
* handler.
|
||||
* A helper for extracting parameters needed by this flow navigation handler.
|
||||
*/
|
||||
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. <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. <p> The default implementation simply exposes all request
|
||||
* parameters as flow execution input attributes. May be null.
|
||||
* 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.
|
||||
* <p>
|
||||
* The default implementation simply exposes all request parameters as flow execution input attributes. May be null.
|
||||
*/
|
||||
private AttributeMapper inputMapper = new RequestParameterInputMapper();
|
||||
|
||||
@@ -100,10 +105,8 @@ public class FlowNavigationHandler extends DecoratingNavigationHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link FlowNavigationHandler}, wrapping the specified
|
||||
* standard navigation handler implementation.
|
||||
* @param originalNavigationHandler Standard <code>NavigationHandler</code>
|
||||
* we are wrapping
|
||||
* Create a new {@link FlowNavigationHandler}, wrapping the specified standard navigation handler implementation.
|
||||
* @param originalNavigationHandler Standard <code>NavigationHandler</code> we are wrapping
|
||||
*/
|
||||
public FlowNavigationHandler(NavigationHandler originalNavigationHandler) {
|
||||
super(originalNavigationHandler);
|
||||
@@ -117,8 +120,8 @@ public class FlowNavigationHandler extends DecoratingNavigationHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the argument extractor to use by this navigation handler. Call to
|
||||
* customize how flow id and event id arguments are extracted.
|
||||
* 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;
|
||||
@@ -132,10 +135,10 @@ public class FlowNavigationHandler extends DecoratingNavigationHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
* @see RequestParameterInputMapper
|
||||
*/
|
||||
public void setInputMapper(AttributeMapper inputMapper) {
|
||||
@@ -145,7 +148,9 @@ public class FlowNavigationHandler extends DecoratingNavigationHandler {
|
||||
public void handleNavigation(FacesContext facesContext, String fromAction, String outcome,
|
||||
NavigationHandler originalNavigationHandler) {
|
||||
try {
|
||||
JsfExternalContext context = new JsfExternalContext(facesContext, fromAction, outcome);
|
||||
JsfExternalContext context = getCurrentContext();
|
||||
// record the navigation handler context
|
||||
context.handleNavigationCalled(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
|
||||
@@ -173,12 +178,13 @@ public class FlowNavigationHandler extends DecoratingNavigationHandler {
|
||||
if (argumentExtractor.isEventIdPresent(context)) {
|
||||
// signal the event against the current flow execution
|
||||
String eventId = argumentExtractor.extractEventId(context);
|
||||
try {
|
||||
try {
|
||||
FlowExecutionHolder holder = FlowExecutionHolderUtils.getFlowExecutionHolder(facesContext);
|
||||
ViewSelection selectedView = holder.getFlowExecution().signalEvent(eventId, context);
|
||||
// set the next view to render
|
||||
holder.setViewSelection(selectedView);
|
||||
} catch (NoMatchingTransitionException e) {
|
||||
}
|
||||
catch (NoMatchingTransitionException e) {
|
||||
// not a valid event in the current state: proceed with standard navigation
|
||||
originalNavigationHandler.handleNavigation(facesContext, fromAction, outcome);
|
||||
}
|
||||
@@ -191,19 +197,18 @@ public class FlowNavigationHandler extends DecoratingNavigationHandler {
|
||||
}
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
FlowExecutionHolderUtils.unlockCurrentFlowExecutionIfNecessary(facesContext);
|
||||
cleanupResources(facesContext);
|
||||
throw e;
|
||||
}
|
||||
catch (Error e) {
|
||||
FlowExecutionHolderUtils.unlockCurrentFlowExecutionIfNecessary(facesContext);
|
||||
cleanupResources(facesContext);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -220,6 +225,10 @@ public class FlowNavigationHandler extends DecoratingNavigationHandler {
|
||||
|
||||
// helpers
|
||||
|
||||
private JsfExternalContext getCurrentContext() {
|
||||
return (JsfExternalContext) ExternalContextHolder.getExternalContext();
|
||||
}
|
||||
|
||||
private FlowDefinitionLocator getLocator(JsfExternalContext context) {
|
||||
return FlowFacesUtils.getDefinitionLocator(context.getFacesContext());
|
||||
}
|
||||
@@ -227,4 +236,9 @@ public class FlowNavigationHandler extends DecoratingNavigationHandler {
|
||||
private FlowExecutionFactory getFactory(JsfExternalContext context) {
|
||||
return FlowFacesUtils.getExecutionFactory(context.getFacesContext());
|
||||
}
|
||||
|
||||
private void cleanupResources(FacesContext context) {
|
||||
FlowExecutionHolderUtils.unlockCurrentFlowExecutionIfNecessary(context);
|
||||
ExternalContextHolder.setExternalContext(null);
|
||||
}
|
||||
}
|
||||
@@ -115,8 +115,7 @@ public class FlowPhaseListener implements PhaseListener {
|
||||
* <li>Generating external URLs to redirect to on a ExternalRedirect repsonse.
|
||||
* </ul>
|
||||
* </ol>
|
||||
* How arguments are extracted and how URLs are generated can be customized by setting a custom
|
||||
* {{@link #setArgumentHandler(FlowExecutorArgumentHandler) argument handler}.
|
||||
* How arguments are extracted and how URLs are generated can be customized by setting a custom {{@link #setArgumentHandler(FlowExecutorArgumentHandler) argument handler}.
|
||||
*/
|
||||
private FlowExecutorArgumentHandler argumentHandler = new RequestParameterFlowExecutorArgumentHandler();
|
||||
|
||||
@@ -224,45 +223,53 @@ public class FlowPhaseListener implements PhaseListener {
|
||||
public void beforePhase(PhaseEvent event) {
|
||||
FacesContext context = event.getFacesContext();
|
||||
if (event.getPhaseId() == PhaseId.RESTORE_VIEW) {
|
||||
ExternalContextHolder.setExternalContext(new JsfExternalContext(context));
|
||||
restoreFlowExecution(event.getFacesContext());
|
||||
try {
|
||||
restoreFlowExecution(event.getFacesContext());
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
// clear the current external context only - no lock is acquired at this point
|
||||
ExternalContextHolder.setExternalContext(null);
|
||||
throw e;
|
||||
}
|
||||
catch (Error e) {
|
||||
ExternalContextHolder.setExternalContext(null);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
else if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) {
|
||||
if (FlowExecutionHolderUtils.isFlowExecutionRestored(event.getFacesContext())) {
|
||||
try {
|
||||
prepareResponse(getCurrentContext(), FlowExecutionHolderUtils.getFlowExecutionHolder(context));
|
||||
} catch (RuntimeException e) {
|
||||
FlowExecutionHolderUtils.unlockCurrentFlowExecutionIfNecessary(context);
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
cleanupResources(context);
|
||||
throw e;
|
||||
}
|
||||
catch (Error e) {
|
||||
cleanupResources(context);
|
||||
throw e;
|
||||
} catch (Error e) {
|
||||
FlowExecutionHolderUtils.unlockCurrentFlowExecutionIfNecessary(context);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void afterPhase(PhaseEvent event) {
|
||||
FacesContext context = event.getFacesContext();
|
||||
FacesContext context = event.getFacesContext();
|
||||
if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) {
|
||||
try {
|
||||
if (FlowExecutionHolderUtils.isFlowExecutionRestored(context)) {
|
||||
FlowExecutionHolder holder = FlowExecutionHolderUtils.getFlowExecutionHolder(context);
|
||||
try {
|
||||
saveFlowExecution(getCurrentContext(), holder);
|
||||
}
|
||||
finally {
|
||||
FlowExecutionHolderUtils.unlockCurrentFlowExecutionIfNecessary(context);
|
||||
}
|
||||
if (FlowExecutionHolderUtils.isFlowExecutionRestored(context)) {
|
||||
try {
|
||||
saveFlowExecution(getCurrentContext(), FlowExecutionHolderUtils.getFlowExecutionHolder(context));
|
||||
}
|
||||
finally {
|
||||
// always cleanup after save - done with flow execution request processing
|
||||
cleanupResources(context);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
ExternalContextHolder.setExternalContext(null);
|
||||
}
|
||||
} else {
|
||||
// unlock if some other JSF artifact marked 'response complete' to short-circuit the lifecycle early
|
||||
}
|
||||
else {
|
||||
// cleanup if some other JSF artifact marked 'response complete' to short-circuit the lifecycle early
|
||||
if (context.getResponseComplete()) {
|
||||
FlowExecutionHolderUtils.unlockCurrentFlowExecutionIfNecessary(context);
|
||||
cleanupResources(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,7 +277,7 @@ public class FlowPhaseListener implements PhaseListener {
|
||||
protected void restoreFlowExecution(FacesContext facesContext) {
|
||||
JsfExternalContext context = new JsfExternalContext(facesContext);
|
||||
if (argumentHandler.isFlowExecutionKeyPresent(context)) {
|
||||
// restore flow execution from repository so it will be available to other JSF artifacts
|
||||
// restore flow execution from repository so it will be available togother JSF artifacts
|
||||
// (this could happen as part of a flow execution redirect or browser refresh)
|
||||
FlowExecutionRepository repository = getRepository(context);
|
||||
FlowExecutionKey flowExecutionKey = repository.parseFlowExecutionKey(argumentHandler.extractFlowExecutionKey(context));
|
||||
@@ -281,12 +288,14 @@ public class FlowPhaseListener implements PhaseListener {
|
||||
flowExecution = repository.getFlowExecution(flowExecutionKey);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Loaded existing flow execution key '" + flowExecutionKey + "' due to browser access "
|
||||
+ "[either via a flow execution redirect or direct browser refresh]");
|
||||
+ "[either via a flow execution redirect or direct browser refresh]");
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
lock.unlock();
|
||||
throw e;
|
||||
} catch (Error e) {
|
||||
}
|
||||
catch (Error e) {
|
||||
lock.unlock();
|
||||
throw e;
|
||||
}
|
||||
@@ -339,7 +348,8 @@ public class FlowPhaseListener implements PhaseListener {
|
||||
// no navigation event has been processed - simply refresh the execution with the same key
|
||||
selectedView = holder.getFlowExecution().refresh(context);
|
||||
holder.setViewSelection(selectedView);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// an navigation event has been processed - generate a new flow execution key if necessary
|
||||
generateKey(context, holder);
|
||||
}
|
||||
@@ -428,6 +438,11 @@ public class FlowPhaseListener implements PhaseListener {
|
||||
return (JsfExternalContext) ExternalContextHolder.getExternalContext();
|
||||
}
|
||||
|
||||
private void cleanupResources(FacesContext context) {
|
||||
FlowExecutionHolderUtils.unlockCurrentFlowExecutionIfNecessary(context);
|
||||
ExternalContextHolder.setExternalContext(null);
|
||||
}
|
||||
|
||||
private void updateViewRoot(FacesContext facesContext, String viewId) {
|
||||
UIViewRoot viewRoot = facesContext.getViewRoot();
|
||||
if (viewRoot == null || hasViewChanged(viewRoot, viewId)) {
|
||||
@@ -455,7 +470,7 @@ public class FlowPhaseListener implements PhaseListener {
|
||||
FlowExecutionKeyStateHolder.COMPONENT_ID);
|
||||
if (keyHolder == null) {
|
||||
keyHolder = new FlowExecutionKeyStateHolder();
|
||||
// expose as the in the view root for preservation in the tree
|
||||
// expose in the view root for preservation in the component tree
|
||||
facesContext.getViewRoot().getChildren().add(keyHolder);
|
||||
}
|
||||
keyHolder.setFlowExecutionKey(flowExecutionKey);
|
||||
|
||||
@@ -78,19 +78,6 @@ public class JsfExternalContext implements ExternalContext {
|
||||
initMaps(facesContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSF External Context.
|
||||
* @param facesContext the JSF faces context.
|
||||
* @param actionId the action that fired
|
||||
* @param outcome the action outcome
|
||||
*/
|
||||
public JsfExternalContext(FacesContext facesContext, String actionId, String outcome) {
|
||||
this.facesContext = facesContext;
|
||||
this.actionId = actionId;
|
||||
this.outcome = outcome;
|
||||
initMaps(facesContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes parameter and attribute maps from context data structures.
|
||||
* @param facesContext the faces context
|
||||
@@ -155,6 +142,16 @@ public class JsfExternalContext implements ExternalContext {
|
||||
return outcome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the action and outcome context information when navigation handling occurs.
|
||||
* @param actionId the from action identifier
|
||||
* @param outcome the action outcome
|
||||
*/
|
||||
public void handleNavigationCalled(String actionId, String outcome) {
|
||||
this.actionId = actionId;
|
||||
this.outcome = outcome;
|
||||
}
|
||||
|
||||
/**
|
||||
* An accessor of a JSF session map.
|
||||
* @author Keith Donald
|
||||
|
||||
@@ -27,19 +27,20 @@ public class FlowNavigationHandlerArgumentExtractorTests extends TestCase {
|
||||
|
||||
protected void setUp() throws Exception {
|
||||
extractor = new FlowNavigationHandlerArgumentExtractor();
|
||||
|
||||
facesContext = new MockFacesContext();
|
||||
facesContext.setExternalContext(new MockJsfExternalContext());
|
||||
}
|
||||
|
||||
public void testExtractFlowId() {
|
||||
JsfExternalContext context = new JsfExternalContext(facesContext, "action", "flowId:foo");
|
||||
JsfExternalContext context = new JsfExternalContext(facesContext);
|
||||
context.handleNavigationCalled("action", "flowId:foo");
|
||||
String flowId = extractor.extractFlowId(context);
|
||||
assertEquals("Wrong flow id", "foo", flowId);
|
||||
}
|
||||
|
||||
public void testExtractFlowIdWrongFormat() {
|
||||
JsfExternalContext context = new JsfExternalContext(facesContext, "action", "flow:foo");
|
||||
JsfExternalContext context = new JsfExternalContext(facesContext);
|
||||
context.handleNavigationCalled("action", "bogus:foo");
|
||||
try {
|
||||
extractor.extractFlowId(context);
|
||||
fail();
|
||||
@@ -50,7 +51,8 @@ public class FlowNavigationHandlerArgumentExtractorTests extends TestCase {
|
||||
}
|
||||
|
||||
public void testExtractEventId() {
|
||||
JsfExternalContext context = new JsfExternalContext(facesContext, "action", "submit");
|
||||
JsfExternalContext context = new JsfExternalContext(facesContext);
|
||||
context.handleNavigationCalled("action", "submit");
|
||||
String eventId = extractor.extractEventId(context);
|
||||
assertEquals("Wrong event id", "submit", eventId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user