From 91e8a96c11670aec987cdcb09115479614c0ab15 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 23 Jul 2012 09:13:52 -0700 Subject: [PATCH] Add option to save Web Flow output to MVC flash scope Output from a flow can now be saved to the flash scope of Spring MVC when the 'saveOutputToFlashScopeOnRedirect' property of the FlowHandlerAdapter is 'true'. For backwards compatibility this is an opt-in feature that must be configured by the user. Issues: SWF-1561 --- .../mvc/servlet/FlowHandlerAdapter.java | 90 +++++-- .../mvc/servlet/FlowHandlerAdapterTests.java | 222 +++++++----------- src/reference/spring-mvc.xml | 27 ++- src/reference/whatsnew.xml | 6 + 4 files changed, 190 insertions(+), 155 deletions(-) diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/servlet/FlowHandlerAdapter.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/servlet/FlowHandlerAdapter.java index 2585bd25..26ff23b0 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/mvc/servlet/FlowHandlerAdapter.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/servlet/FlowHandlerAdapter.java @@ -27,9 +27,14 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.js.ajax.AjaxHandler; import org.springframework.js.ajax.SpringJavascriptAjaxHandler; import org.springframework.util.Assert; +import org.springframework.web.servlet.FlashMap; +import org.springframework.web.servlet.FlashMapManager; import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.servlet.support.WebContentGenerator; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; import org.springframework.webflow.context.servlet.DefaultFlowUrlHandler; import org.springframework.webflow.context.servlet.FlowUrlHandler; import org.springframework.webflow.context.servlet.ServletExternalContext; @@ -45,8 +50,9 @@ import org.springframework.webflow.executor.FlowExecutor; * A custom MVC HandlerAdapter that encapsulates the generic workflow associated with executing flows in a Servlet * environment. Delegates to mapped {@link FlowHandler flow handlers} to manage the interaction with executions of * specific flow definitions. - * + * * @author Keith Donald + * @author Phillip Webb */ public class FlowHandlerAdapter extends WebContentGenerator implements HandlerAdapter, InitializingBean { @@ -77,6 +83,8 @@ public class FlowHandlerAdapter extends WebContentGenerator implements HandlerAd private boolean redirectHttp10Compatible = true; + private boolean saveOutputToFlashScopeOnRedirect; + /** * Creates a new flow handler adapter. * @see #setFlowExecutor(FlowExecutor) @@ -157,6 +165,32 @@ public class FlowHandlerAdapter extends WebContentGenerator implements HandlerAd this.redirectHttp10Compatible = redirectHttp10Compatible; } + /** + * Set whether servlet relative redirects sent by this handler adapter + * should pass {@link FlowExecutionOutcome#getOutput() flow output} to the + * Spring MVC {@link FlashMap flash scope}. + * + *

By default, to remain compatible with previous releases, flow output is + * not mapped to flash scope. + * + * @param saveOutputToFlashScopeOnRedirect + * @see #getFlashOutput(HttpServletRequest, HttpServletResponse, FlowExecutionResult) + */ + public void setSaveOutputToFlashScopeOnRedirect(boolean saveOutputToFlashScopeOnRedirect) { + this.saveOutputToFlashScopeOnRedirect = saveOutputToFlashScopeOnRedirect; + } + + /** + * Whether servlet relative redirects should pass + * {@link FlowExecutionOutcome#getOutput() flow output} to the Spring MVC + * {@link FlashMap flash scope}. + * + * @return {@code true} if so, {@code false} otherwise + */ + public boolean getSaveOutputToFlashScopeOnRedirect() { + return this.saveOutputToFlashScopeOnRedirect; + } + public void afterPropertiesSet() throws Exception { Assert.notNull(flowExecutor, "The FlowExecutor to execute flows is required"); if (flowUrlHandler == null) { @@ -339,11 +373,11 @@ public class FlowHandlerAdapter extends WebContentGenerator implements HandlerAd if (context.getFlowDefinitionRedirectRequested()) { sendFlowDefinitionRedirect(result, context, request, response); } else if (context.getExternalRedirectRequested()) { - sendExternalRedirect(context.getExternalRedirectUrl(), request, response); + sendExternalRedirect(context.getExternalRedirectUrl(), request, response, result); } else { String location = handler.handleExecutionOutcome(result.getOutcome(), request, response); if (location != null) { - sendExternalRedirect(location, request, response); + sendExternalRedirect(location, request, response, result); } else { defaultHandleExecutionOutcome(result.getFlowId(), result.getOutcome(), context, request, response); } @@ -382,15 +416,20 @@ public class FlowHandlerAdapter extends WebContentGenerator implements HandlerAd private void sendExternalRedirect(String location, HttpServletRequest request, HttpServletResponse response) throws IOException { + sendExternalRedirect(location, request, response, null); + } + + private void sendExternalRedirect(String location, HttpServletRequest request, HttpServletResponse response, + FlowExecutionResult result) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Sending external redirect to '" + location + "'"); } if (location.startsWith(SERVLET_RELATIVE_LOCATION_PREFIX)) { sendServletRelativeRedirect(location.substring(SERVLET_RELATIVE_LOCATION_PREFIX.length()), request, - response); + response, result); } else if (location.startsWith(CONTEXT_RELATIVE_LOCATION_PREFIX)) { sendContextRelativeRedirect(location.substring(CONTEXT_RELATIVE_LOCATION_PREFIX.length()), request, - response); + response, result); } else if (location.startsWith(SERVER_RELATIVE_LOCATION_PREFIX)) { String url = location.substring(SERVER_RELATIVE_LOCATION_PREFIX.length()); if (!url.startsWith("/")) { @@ -401,9 +440,9 @@ public class FlowHandlerAdapter extends WebContentGenerator implements HandlerAd sendRedirect(location, request, response); } else { if (isRedirectServletRelative(request)) { - sendServletRelativeRedirect(location, request, response); + sendServletRelativeRedirect(location, request, response, result); } else { - sendContextRelativeRedirect(location, request, response); + sendContextRelativeRedirect(location, request, response, result); } } } @@ -413,7 +452,7 @@ public class FlowHandlerAdapter extends WebContentGenerator implements HandlerAd * such as "contextRelative: was not specified. This answer depends on how the MVC Dispatcher Servlet is mapped: (1) * default servlet, (2) prefix, (3) extension, (4) exact match. In (1), (3), and (4) it doesn't make sense to * prepend the servlet path, which contains the entire URL after the context path. - * + * * Because there is no simple way to get the servlet mapping, this method is implemented to return True if path info * is not null. Also see SWF-1385. */ @@ -421,25 +460,48 @@ public class FlowHandlerAdapter extends WebContentGenerator implements HandlerAd return (request.getPathInfo() != null); } - private void sendContextRelativeRedirect(String location, HttpServletRequest request, HttpServletResponse response) - throws IOException { + private void sendContextRelativeRedirect(String location, HttpServletRequest request, HttpServletResponse response, + FlowExecutionResult result) throws IOException { StringBuilder url = new StringBuilder(request.getContextPath()); if (!location.startsWith("/")) { url.append('/'); } url.append(location); - sendRedirect(url.toString(), request, response); + sendRedirect(url.toString(), request, response, result); } - private void sendServletRelativeRedirect(String location, HttpServletRequest request, HttpServletResponse response) - throws IOException { + private void sendServletRelativeRedirect(String location, HttpServletRequest request, HttpServletResponse response, + FlowExecutionResult result) throws IOException { StringBuilder url = new StringBuilder(request.getContextPath()); url.append(request.getServletPath()); if (!location.startsWith("/")) { url.append('/'); } url.append(location); - sendRedirect(url.toString(), request, response); + sendRedirect(url.toString(), request, response, result); + } + + private void sendRedirect(String url, HttpServletRequest request, HttpServletResponse response, + FlowExecutionResult result) throws IOException { + + if (this.saveOutputToFlashScopeOnRedirect) { + saveFlashOutput(url.toString(), request, response, result); + } + sendRedirect(url, request, response); + } + + private void saveFlashOutput(String location, HttpServletRequest request, HttpServletResponse response, + FlowExecutionResult result) { + Map output = result.getOutcome().getOutput().asMap(); + FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request); + if (flashMapManager != null && output != null && !output.isEmpty()) { + UriComponents uriComponents = UriComponentsBuilder.fromUriString(location).build(); + FlashMap flashMap = new FlashMap(); + flashMap.setTargetRequestPath(uriComponents.getPath()); + flashMap.addTargetRequestParams(uriComponents.getQueryParams()); + flashMap.putAll(output); + flashMapManager.saveOutputFlashMap(flashMap, request, response); + } } private void handleFlowException(FlowException e, HttpServletRequest request, HttpServletResponse response, diff --git a/spring-webflow/src/test/java/org/springframework/webflow/mvc/servlet/FlowHandlerAdapterTests.java b/spring-webflow/src/test/java/org/springframework/webflow/mvc/servlet/FlowHandlerAdapterTests.java index dba7ad10..a292268e 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/mvc/servlet/FlowHandlerAdapterTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/mvc/servlet/FlowHandlerAdapterTests.java @@ -13,6 +13,9 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.FlashMap; +import org.springframework.web.servlet.FlashMapManager; import org.springframework.web.servlet.ModelAndView; import org.springframework.webflow.context.servlet.ServletExternalContext; import org.springframework.webflow.core.FlowException; @@ -34,6 +37,7 @@ public class FlowHandlerAdapterTests extends TestCase { private LocalAttributeMap flowInput = new LocalAttributeMap(); private boolean handleException; private boolean handleExecutionOutcome; + private MockFlashMapManager flashMapManager = new MockFlashMapManager(); protected void setUp() throws Exception { flowExecutor = EasyMock.createMock(FlowExecutor.class); @@ -80,14 +84,11 @@ public class FlowHandlerAdapterTests extends TestCase { request = new MockHttpServletRequest(); response = new MockHttpServletResponse(); context = new ServletExternalContext(servletContext, request, response, flowHandlerAdapter.getFlowUrlHandler()); + request.setAttribute(DispatcherServlet.FLASH_MAP_MANAGER_ATTRIBUTE, flashMapManager); } public void testLaunchFlowRequest() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/whatever"); - request.setRequestURI("/springtravel/app/whatever"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/whatever", "GET"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); EasyMock.expectLastCall().andReturn(result); @@ -97,11 +98,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowRequestEndsAfterProcessing() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/whatever"); - request.setRequestURI("/springtravel/app/whatever"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/whatever", "GET"); Map parameters = new HashMap(); request.setParameters(parameters); flowExecutor.launchExecution("foo", flowInput, context); @@ -117,11 +114,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowRequestEndsAfterProcessingAjaxRequest() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/whatever"); - request.setRequestURI("/springtravel/app/whatever"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/whatever", "GET"); Map parameters = new HashMap(); request.setParameters(parameters); context.setAjaxRequest(true); @@ -139,11 +132,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testResumeFlowRequest() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("POST"); + setupRequest("/springtravel", "/app", "/foo", "POST"); request.addParameter("execution", "12345"); flowExecutor.resumeExecution("12345", context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "123456"); @@ -154,11 +143,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testResumeFlowRequestEndsAfterProcessing() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("POST"); + setupRequest("/springtravel", "/app", "/foo", "POST"); request.addParameter("execution", "12345"); Map parameters = new HashMap(); request.setParameters(parameters); @@ -176,11 +161,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testResumeFlowRequestEndsAfterProcessingFlowCommittedResponse() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("POST"); + setupRequest("/springtravel", "/app", "/foo", "POST"); request.addParameter("execution", "12345"); Map parameters = new HashMap(); request.setParameters(parameters); @@ -199,11 +180,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowWithExecutionRedirect() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); context.requestFlowExecutionRedirect(); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -216,11 +193,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowWithDefinitionRedirect() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); Map parameters = new HashMap(); request.setParameters(parameters); LocalAttributeMap input = new LocalAttributeMap(); @@ -241,11 +214,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowWithExternalHttpRedirect() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); context.requestExternalRedirect("http://www.paypal.com"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -258,11 +227,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowWithExternalHttpsRedirect() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); context.requestExternalRedirect("https://www.paypal.com"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -275,11 +240,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowWithExternalRedirectServletRelative() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); context.requestExternalRedirect("servletRelative:bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -292,11 +253,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowWithExternalRedirectServletRelativeWithSlash() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); context.requestExternalRedirect("servletRelative:/bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -309,11 +266,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowWithExternalRedirectContextRelative() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); context.requestExternalRedirect("contextRelative:bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -326,11 +279,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowWithExternalRedirectContextRelativeWithSlash() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); context.requestExternalRedirect("contextRelative:/bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -343,11 +292,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowWithExternalRedirectServerRelative() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); context.requestExternalRedirect("serverRelative:bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -360,11 +305,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testLaunchFlowWithExternalRedirectServerRelativeWithSlash() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); context.requestExternalRedirect("serverRelative:/bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -378,11 +319,7 @@ public class FlowHandlerAdapterTests extends TestCase { public void testLaunchFlowWithExternalRedirectNotHttp10Compatible() throws Exception { flowHandlerAdapter.setRedirectHttp10Compatible(false); - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); context.requestExternalRedirect("serverRelative:/bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -396,14 +333,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testSwf1385DefaultServletExternalRedirect() throws Exception { - // The default case in accordance with the servlet spec: - // "A string containing only the ’/’ character indicates the "default" servlet of the application. - // In this case the servlet path is the request URI minus the context path and the path info is null." - request.setContextPath("/springtravel"); - request.setServletPath("/foo"); - request.setPathInfo(null); - request.setRequestURI("/springtravel/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/foo", null, "GET"); context.requestExternalRedirect("/bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -419,11 +349,7 @@ public class FlowHandlerAdapterTests extends TestCase { // Deviation from the default case: // In some containers the default behavior can be switched so that the contents of the URI after // the context path is in the path info while the servlet path is empty. - request.setContextPath("/springtravel"); - request.setServletPath(""); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "", "/foo", "GET"); context.requestExternalRedirect("/bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -436,10 +362,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testSwf1385DefaultServletExternalRedirectServletRelative() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/foo"); - request.setRequestURI("/springtravel/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/foo", null, "GET"); context.requestExternalRedirect("/bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -452,10 +375,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testExternalRedirectServletRelativeWithDefaultServletMapping() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/foo"); - request.setRequestURI("/springtravel/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/foo", null, "GET"); context.requestExternalRedirect("servletRelative:bar"); flowExecutor.launchExecution("foo", flowInput, context); FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345"); @@ -468,11 +388,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testDefaultHandleFlowException() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); Map parameters = new HashMap(); request.setParameters(parameters); flowExecutor.launchExecution("foo", flowInput, context); @@ -490,11 +406,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testDefaultHandleNoSuchFlowExecutionException() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); request.addParameter("execution", "12345"); flowExecutor.resumeExecution("12345", context); FlowException flowException = new NoSuchFlowExecutionException(new MockFlowExecutionKey("12345"), null); @@ -506,11 +418,7 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testDefaultHandleNoSuchFlowExecutionExceptionAjaxRequest() throws Exception { - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); request.addParameter("execution", "12345"); flowExecutor.resumeExecution("12345", context); FlowException flowException = new NoSuchFlowExecutionException(new MockFlowExecutionKey("12345"), null); @@ -524,12 +432,37 @@ public class FlowHandlerAdapterTests extends TestCase { } public void testHandleFlowOutcomeCustomFlowHandler() throws Exception { + doHandleFlowServletRedirectOutcome(); + EasyMock.verify(new Object[] { flowExecutor }); + } + + public void testHandleFlowExceptionCustomFlowHandler() throws Exception { + handleException = true; + final FlowException flowException = new FlowException("Error") { + }; + setupRequest("/springtravel", "/app", "/foo", "GET"); + flowExecutor.launchExecution("foo", flowInput, context); + EasyMock.expectLastCall().andThrow(flowException); + EasyMock.replay(new Object[] { flowExecutor }); + flowHandlerAdapter.handle(request, response, flowHandler); + EasyMock.verify(new Object[] { flowExecutor }); + } + + public void testHandleFlowServletRedirectOutcomeWithoutFlash() throws Exception { + doHandleFlowServletRedirectOutcome(); + assertNull(flashMapManager.getFlashMap()); + } + + public void testHandleFlowServletRedirectOutcomeWithFlash() throws Exception { + flowHandlerAdapter.setSaveOutputToFlashScopeOnRedirect(true); + doHandleFlowServletRedirectOutcome(); + assertEquals("baz", flashMapManager.getFlashMap().get("bar")); + assertEquals("/springtravel/app/home", flashMapManager.getFlashMap().getTargetRequestPath()); + } + + private void doHandleFlowServletRedirectOutcome() throws Exception { handleExecutionOutcome = true; - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); + setupRequest("/springtravel", "/app", "/foo", "GET"); flowExecutor.launchExecution("foo", flowInput, context); LocalAttributeMap output = new LocalAttributeMap(); output.put("bar", "baz"); @@ -541,19 +474,28 @@ public class FlowHandlerAdapterTests extends TestCase { EasyMock.verify(new Object[] { flowExecutor }); } - public void testHandleFlowExceptionCustomFlowHandler() throws Exception { - handleException = true; - final FlowException flowException = new FlowException("Error") { - }; - request.setContextPath("/springtravel"); - request.setServletPath("/app"); - request.setPathInfo("/foo"); - request.setRequestURI("/springtravel/app/foo"); - request.setMethod("GET"); - flowExecutor.launchExecution("foo", flowInput, context); - EasyMock.expectLastCall().andThrow(flowException); - EasyMock.replay(new Object[] { flowExecutor }); - flowHandlerAdapter.handle(request, response, flowHandler); - EasyMock.verify(new Object[] { flowExecutor }); + private void setupRequest(String contextPath, String servletPath, String pathInfo, String method) { + request.setContextPath(contextPath); + request.setServletPath(servletPath); + request.setPathInfo(pathInfo); + request.setRequestURI(contextPath + servletPath + (pathInfo == null ? "" : pathInfo)); + request.setMethod(method); + } + + private static class MockFlashMapManager implements FlashMapManager { + + private FlashMap flashMap; + + public FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response) { + throw new UnsupportedOperationException(); + } + + public void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response) { + this.flashMap = flashMap; + } + + public FlashMap getFlashMap() { + return flashMap; + } } } diff --git a/src/reference/spring-mvc.xml b/src/reference/spring-mvc.xml index 637180bd..3dd7623a 100644 --- a/src/reference/spring-mvc.xml +++ b/src/reference/spring-mvc.xml @@ -469,6 +469,31 @@ mvn package # import into Eclipse - + + + Saving Flow Output to MVC Flash Scope + Flow output can be automatically saved to MVC flash scope when an end-state + performs an internal redirect. This is particularly useful when displaying a summary + screen at the end of a flow. For backwards compatibility this feature is disabled by + default, to enable set saveOutputToFlashScopeOnRedirect on your + FlowHandlerAdapter to true. + + +<!-- Enables FlowHandler URL mapping --> +<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter"> + <property name="flowExecutor" ref="flowExecutor" /> + <property name="saveOutputToFlashScopeOnRedirect" value="true" /> +</bean> + + + The following example will add confirmationNumber to the MVC flash scope + before redirecting to the summary screen. + + +<end-state id="finish" view="externalRedirect:summary"> + <output name="confirmationNumber" value="booking.confirmationNumber" /> +</end-state> + + diff --git a/src/reference/whatsnew.xml b/src/reference/whatsnew.xml index 67131a90..9488e437 100644 --- a/src/reference/whatsnew.xml +++ b/src/reference/whatsnew.xml @@ -23,6 +23,12 @@ included as part of their Spring configuration. See . + + Saving Flow Output to Spring MVC Flash Scope + + Flow output can now be saved to Spring MVC Flash Scope for any end-state that issues an internal redirect. To enable this feature set FlowHandlerAdapter.saveOutputToFlashScopeOnRedirect. See . + + Spring Web Flow 2.3