From 8a69dcc9f4b55432627ccbd562fc13f71b4dce58 Mon Sep 17 00:00:00 2001
From: Keith Donald
+ *
* 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:
* 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.
- *
- *
+ *
- * 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: + *
NavigationHandler implementation and return.
+ * 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