refinements related to ajax / popup menus

This commit is contained in:
Keith Donald
2008-03-08 22:40:07 +00:00
parent e7e7b95ec4
commit a1d9f73ca9
10 changed files with 293 additions and 79 deletions

View File

@@ -85,7 +85,7 @@ public interface ExternalContext {
public SharedAttributeMap getApplicationMap();
/**
* Returns true if the current request is an Ajax request.
* Returns true if the current request is an asynchronous Ajax request.
* @return true if the current request is an Ajax request
*/
public boolean isAjaxRequest();
@@ -123,13 +123,6 @@ public interface ExternalContext {
*/
public Writer getResponseWriter();
/**
* Sets an entry in the response header.
* @param name the entry name
* @param value the entry value
*/
public void setResponseHeader(String name, String value);
/**
* Request that a flow execution redirect be performed by the calling environment. Typically called from within a
* flow execution to request a refresh operation, usually to support "refresh after event processing" behavior.
@@ -156,6 +149,15 @@ public interface ExternalContext {
*/
public void requestExternalRedirect(String uri);
/**
* Request that the redirect response requested be sent to the client in a manner that causes the client to issue
* the redirect from a popup dialog. Calling this method only has an effect when a redirect has been requested.
* @see #requestFlowExecutionRedirect()
* @see #requestFlowDefinitionRedirect(String, AttributeMap)
* @see #requestExternalRedirect(String)
*/
public void requestRedirectInPopup();
/**
* Has the response been committed?
* @return true if yes, false otherwise

View File

@@ -22,7 +22,6 @@ import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.util.StringUtils;
import org.springframework.webflow.context.ExternalContext;
import org.springframework.webflow.core.collection.AttributeMap;
import org.springframework.webflow.core.collection.LocalAttributeMap;
@@ -41,12 +40,6 @@ import org.springframework.webflow.core.collection.SharedAttributeMap;
*/
public class ServletExternalContext implements ExternalContext {
/** The accept header value that signifies an Ajax request */
private static final String AJAX_ACCEPT_CONTENT_TYPE = "text/html;type=ajax";
/** Alternate request parameter to indicate an Ajax request for cases when control of the header is not available */
private static final String AJAX_SOURCE_PARAM = "ajaxSource";
/**
* The context.
*/
@@ -109,6 +102,17 @@ public class ServletExternalContext implements ExternalContext {
*/
private FlowUrlHandler flowUrlHandler;
/**
* Whether this external request context originated from an Ajax request or not.
*/
private boolean ajaxRequest;
/**
* In the case where a redirect response is requested, this flag indicates if the redirect should be issued from a
* popup dialog.
*/
private boolean redirectInPopup;
/**
* Create a new external context wrapping given servlet HTTP request and response and given servlet context.
* @param context the servlet context
@@ -131,6 +135,15 @@ public class ServletExternalContext implements ExternalContext {
init(context, request, response, flowUrlHandler);
}
/**
* Indicates if the current request from this client is an ajax request. This flag may effect the handling of
* response writing within Spring Web Flow.
* @param ajaxRequest the ajax request flag
*/
public void setAjaxRequest(boolean ajaxRequest) {
this.ajaxRequest = ajaxRequest;
}
// implementing external context
public String getContextPath() {
@@ -170,13 +183,7 @@ public class ServletExternalContext implements ExternalContext {
}
public boolean isAjaxRequest() {
String acceptHeader = request.getHeader("Accept");
String ajaxParam = request.getParameter(AJAX_SOURCE_PARAM);
if (AJAX_ACCEPT_CONTENT_TYPE.equals(acceptHeader) || StringUtils.hasText(ajaxParam)) {
return true;
} else {
return false;
}
return ajaxRequest;
}
public String getFlowExecutionUri(String flowId, String flowExecutionKey) {
@@ -191,10 +198,6 @@ public class ServletExternalContext implements ExternalContext {
}
}
public void setResponseHeader(String name, String value) {
response.setHeader(name, value);
}
public boolean isResponseCommitted() {
return flowExecutionRedirectRequested() || flowDefinitionRedirectRequested() || externalRedirectRequested();
}
@@ -212,32 +215,63 @@ public class ServletExternalContext implements ExternalContext {
flowDefinitionRedirectFlowInput = input;
}
public void requestRedirectInPopup() {
redirectInPopup = true;
}
// implementation specific methods
/**
* Returns the flag indicating if a flow execution redirect response has been requested by the flow.
*/
public boolean flowExecutionRedirectRequested() {
return flowExecutionRedirectRequested;
}
/**
* Returns the flag indicating if a flow definition redirect response has been requested by the flow.
*/
public boolean flowDefinitionRedirectRequested() {
return flowDefinitionRedirectFlowId != null;
}
/**
* Returns the id of the flow definition to redirect to. Only set when {@link #flowDefinitionRedirectRequested()}
* returns true.
*/
public String getFlowRedirectFlowId() {
return flowDefinitionRedirectFlowId;
}
/**
* Returns the input to pass the flow definition through the redirect. Only set when
* {@link #flowDefinitionRedirectRequested()} returns true.
*/
public AttributeMap getFlowRedirectFlowInput() {
return flowDefinitionRedirectFlowInput;
}
/**
* Returns the flag indicating if an external redirect response has been requested by the flow.
*/
public boolean externalRedirectRequested() {
return externalRedirectUrl != null;
}
/**
* Returns the URL to redirect to. Only set if {@link #externalRedirectRequested()} returns true.
*/
public String getExternalRedirectUrl() {
return externalRedirectUrl;
}
/**
* If a redirect response has been requested, indicates if the redirect should be issued from a popup dialog.
*/
public boolean redirectInPopup() {
return redirectInPopup;
}
// private helpers
private void init(ServletContext context, HttpServletRequest request, HttpServletResponse response,

View File

@@ -117,11 +117,4 @@ public interface RequestControlContext extends RequestContext {
*/
public boolean getAlwaysRedirectOnPause();
/**
* Request that a redirect be sent to this flow execution after the current request has processed. The current flow
* execution must have its key assigned for this operation to be supported.
* @throws IllegalStateException if the flow execution has not yet had its key assigned
*/
public void sendFlowExecutionRedirect() throws IllegalStateException;
}

View File

@@ -38,8 +38,6 @@ import org.springframework.webflow.execution.ViewFactory;
*/
public class ViewState extends TransitionableState {
private static final String POPUP_VIEW_HEADER = "Flow-Modal-View";
/**
* The list of actions to be executed when this state is entered.
*/
@@ -163,10 +161,10 @@ public class ViewState extends TransitionableState {
protected void doEnter(RequestControlContext context) throws FlowExecutionException {
context.assignFlowExecutionKey();
if (shouldRedirect(context)) {
if (context.getExternalContext().isAjaxRequest() && popup) {
context.getExternalContext().setResponseHeader(POPUP_VIEW_HEADER, "true");
context.getExternalContext().requestFlowExecutionRedirect();
if (popup) {
context.getExternalContext().requestRedirectInPopup();
}
context.sendFlowExecutionRedirect();
} else {
View view = viewFactory.getView(context);
render(context, view);

View File

@@ -9,6 +9,7 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import org.springframework.webflow.context.servlet.DefaultFlowUrlHandler;
@@ -28,6 +29,26 @@ import org.springframework.webflow.executor.FlowExecutor;
*/
public class FlowController extends AbstractController {
/**
* The response header to be set on an Ajax redirect
*/
private static final String FLOW_REDIRECT_URL_HEADER = "Flow-Redirect-URL";
/**
* The response header to be set on an redirect that should be issued from a popup window.
*/
private static final String POPUP_VIEW_HEADER = "Flow-Modal-View";
/**
* The accept header value that signifies an Ajax request.
*/
private static final String AJAX_ACCEPT_CONTENT_TYPE = "text/html;type=ajax";
/**
* Alternate request parameter to indicate an Ajax request for cases when control of the header is not available.
*/
private static final String AJAX_SOURCE_PARAM = "ajaxSource";
private static final Log logger = LogFactory.getLog(FlowController.class);
private FlowExecutor flowExecutor;
@@ -36,9 +57,6 @@ public class FlowController extends AbstractController {
private Map flowHandlers = new HashMap();
/** The response header to be set on an Ajax redirect */
private static final String FLOW_REDIRECT_URL_HEADER = "Flow-Redirect-URL";
/**
* @param flowExecutor the web flow executor service
*/
@@ -89,7 +107,19 @@ public class FlowController extends AbstractController {
protected ServletExternalContext createServletExternalContext(HttpServletRequest request,
HttpServletResponse response) {
return new ServletExternalContext(getServletContext(), request, response, urlHandler);
ServletExternalContext context = new ServletExternalContext(getServletContext(), request, response, urlHandler);
context.setAjaxRequest(isAjaxRequest(request));
return context;
}
protected boolean isAjaxRequest(HttpServletRequest request) {
String acceptHeader = request.getHeader("Accept");
String ajaxParam = request.getParameter(AJAX_SOURCE_PARAM);
if (AJAX_ACCEPT_CONTENT_TYPE.equals(acceptHeader) || StringUtils.hasText(ajaxParam)) {
return true;
} else {
return false;
}
}
protected MutableAttributeMap defaultFlowExecutionInputMap(HttpServletRequest request) {
@@ -170,7 +200,10 @@ public class FlowController extends AbstractController {
private void sendRedirect(ServletExternalContext context, HttpServletResponse response, String targetUrl)
throws IOException {
if (context.isAjaxRequest()) {
context.setResponseHeader(FLOW_REDIRECT_URL_HEADER, response.encodeRedirectURL(targetUrl));
if (context.redirectInPopup()) {
response.setHeader(POPUP_VIEW_HEADER, "true");
}
response.setHeader(FLOW_REDIRECT_URL_HEADER, response.encodeRedirectURL(targetUrl));
} else {
response.sendRedirect(response.encodeRedirectURL(targetUrl));
}

View File

@@ -7,6 +7,7 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.WebApplicationObjectSupport;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.ModelAndView;
@@ -23,15 +24,32 @@ import org.springframework.webflow.executor.FlowExecutor;
public class FlowHandlerAdapter extends WebApplicationObjectSupport implements HandlerAdapter {
/**
* The response header to be set on an Ajax redirect
*/
private static final String FLOW_REDIRECT_URL_HEADER = "Flow-Redirect-URL";
/**
* The response header to be set on an redirect that should be issued from a popup window.
*/
private static final String POPUP_VIEW_HEADER = "Flow-Modal-View";
/**
* The accept header value that signifies an Ajax request.
*/
private static final String AJAX_ACCEPT_CONTENT_TYPE = "text/html;type=ajax";
/**
* Alternate request parameter to indicate an Ajax request for cases when control of the header is not available.
*/
private static final String AJAX_SOURCE_PARAM = "ajaxSource";
private static final Log logger = LogFactory.getLog(FlowHandlerAdapter.class);
private FlowExecutor flowExecutor;
private FlowUrlHandler urlHandler;
/** The response header to be set on an Ajax redirect */
private static final String FLOW_REDIRECT_URL_HEADER = "Flow-Redirect-URL";
public FlowHandlerAdapter(FlowExecutor flowExecutor) {
this.flowExecutor = flowExecutor;
this.urlHandler = new DefaultFlowUrlHandler();
@@ -77,7 +95,19 @@ public class FlowHandlerAdapter extends WebApplicationObjectSupport implements H
protected ServletExternalContext createServletExternalContext(HttpServletRequest request,
HttpServletResponse response) {
return new ServletExternalContext(getServletContext(), request, response, urlHandler);
ServletExternalContext context = new ServletExternalContext(getServletContext(), request, response, urlHandler);
context.setAjaxRequest(isAjaxRequest(request));
return context;
}
protected boolean isAjaxRequest(HttpServletRequest request) {
String acceptHeader = request.getHeader("Accept");
String ajaxParam = request.getParameter(AJAX_SOURCE_PARAM);
if (AJAX_ACCEPT_CONTENT_TYPE.equals(acceptHeader) || StringUtils.hasText(ajaxParam)) {
return true;
} else {
return false;
}
}
protected MutableAttributeMap defaultFlowExecutionInputMap(HttpServletRequest request) {
@@ -159,7 +189,10 @@ public class FlowHandlerAdapter extends WebApplicationObjectSupport implements H
private void sendRedirect(ServletExternalContext context, HttpServletResponse response, String targetUrl)
throws IOException {
if (context.isAjaxRequest()) {
context.setResponseHeader(FLOW_REDIRECT_URL_HEADER, response.encodeRedirectURL(targetUrl));
if (context.redirectInPopup()) {
response.setHeader(POPUP_VIEW_HEADER, "true");
}
response.setHeader(FLOW_REDIRECT_URL_HEADER, response.encodeRedirectURL(targetUrl));
} else {
response.sendRedirect(response.encodeRedirectURL(targetUrl));
}

View File

@@ -18,7 +18,6 @@ package org.springframework.webflow.test;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import org.springframework.binding.collection.SharedMapDecorator;
import org.springframework.webflow.context.ExternalContext;
@@ -58,8 +57,6 @@ public class MockExternalContext implements ExternalContext {
private boolean ajaxRequest;
private Map responseHeaders = new HashMap();
private boolean flowExecutionRedirectRequested;
private String flowDefinitionRedirectFlowId;
@@ -68,6 +65,8 @@ public class MockExternalContext implements ExternalContext {
private String externalRedirectUrl;
private boolean redirectInPopup;
/**
* Creates a mock external context with an empty request parameter map. Allows for bean style usage.
*/
@@ -135,10 +134,6 @@ public class MockExternalContext implements ExternalContext {
return responseWriter;
}
public void setResponseHeader(String name, String value) {
this.responseHeaders.put(name, value);
}
public boolean isResponseCommitted() {
return flowExecutionRedirectRequested() || flowDefinitionRedirectRequested() || externalRedirectRequested();
}
@@ -147,13 +142,17 @@ public class MockExternalContext implements ExternalContext {
flowExecutionRedirectRequested = true;
}
public void requestFlowDefinitionRedirect(String flowId, AttributeMap input) {
flowDefinitionRedirectFlowId = flowId;
flowDefinitionRedirectFlowInput = input;
}
public void requestExternalRedirect(String uri) {
externalRedirectUrl = uri;
}
public void requestFlowDefinitionRedirect(String flowId, AttributeMap input) {
flowDefinitionRedirectFlowId = flowId;
flowDefinitionRedirectFlowInput = input;
public void requestRedirectInPopup() {
redirectInPopup = true;
}
/**
@@ -266,36 +265,54 @@ public class MockExternalContext implements ExternalContext {
}
/**
* Returns the value of the response header entry
* @param name the entry name
* @return the entry value, or null if no entry was set with this name
* Returns the flag indicating if a flow execution redirect response has been requested by the flow.
*/
public String getResponseHeader(String name) {
return (String) responseHeaders.get(name);
}
public boolean flowExecutionRedirectRequested() {
return flowExecutionRedirectRequested;
}
/**
* Returns the flag indicating if a flow definition redirect response has been requested by the flow.
*/
public boolean flowDefinitionRedirectRequested() {
return flowDefinitionRedirectFlowId != null;
}
/**
* Returns the id of the flow definition to redirect to. Only set when {@link #flowDefinitionRedirectRequested()}
* returns true.
*/
public String getFlowRedirectFlowId() {
return flowDefinitionRedirectFlowId;
}
/**
* Returns the input to pass the flow definition through the redirect. Only set when
* {@link #flowDefinitionRedirectRequested()} returns true.
*/
public AttributeMap getFlowRedirectFlowInput() {
return flowDefinitionRedirectFlowInput;
}
/**
* Returns the flag indicating if an external redirect response has been requested by the flow.
*/
public boolean externalRedirectRequested() {
return externalRedirectUrl != null;
}
/**
* Returns the URL to redirect to. Only set if {@link #externalRedirectRequested()} returns true.
*/
public String getExternalRedirectUrl() {
return externalRedirectUrl;
}
/**
* If a redirect response has been requested, indicates if the redirect should be issued from a popup dialog.
*/
public boolean redirectInPopup() {
return redirectInPopup;
}
}

View File

@@ -61,12 +61,7 @@ public class ServletExternalContextTests extends TestCase {
}
public void testAjaxRequestAcceptHeader() {
request.addHeader("Accept", "text/html;type=ajax");
assertTrue(context.isAjaxRequest());
}
public void testAjaxRequestParam() {
request.addParameter("ajaxSource", "myButton");
context.setAjaxRequest(true);
assertTrue(context.isAjaxRequest());
}
@@ -94,4 +89,30 @@ public class ServletExternalContextTests extends TestCase {
assertEquals("foo", context.getExternalRedirectUrl());
}
public void testCommitExecutionRedirectPopup() {
context.requestFlowExecutionRedirect();
context.requestRedirectInPopup();
assertTrue(context.isResponseCommitted());
assertTrue(context.flowExecutionRedirectRequested());
assertTrue(context.redirectInPopup());
}
public void testCommitFlowRedirectPopup() {
context.requestFlowDefinitionRedirect("foo", null);
context.requestRedirectInPopup();
assertTrue(context.isResponseCommitted());
assertTrue(context.flowDefinitionRedirectRequested());
assertEquals("foo", context.getFlowRedirectFlowId());
assertTrue(context.redirectInPopup());
}
public void testCommitExternalRedirectPopup() {
context.requestExternalRedirect("foo");
context.requestRedirectInPopup();
assertTrue(context.isResponseCommitted());
assertTrue(context.externalRedirectRequested());
assertEquals("foo", context.getExternalRedirectUrl());
assertTrue(context.redirectInPopup());
}
}

View File

@@ -19,6 +19,7 @@ import junit.framework.TestCase;
import org.springframework.webflow.engine.support.DefaultTargetStateResolver;
import org.springframework.webflow.engine.support.EventIdTransitionCriteria;
import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.test.MockRequestControlContext;
/**
@@ -27,37 +28,75 @@ import org.springframework.webflow.test.MockRequestControlContext;
*/
public class ViewStateTests extends TestCase {
public void testEnterViewState() {
public void testEnterViewStateDefaultBehavior() {
Flow flow = new Flow("myFlow");
StubViewFactory viewFactory = new StubViewFactory();
ViewState state = new ViewState(flow, "viewState", viewFactory);
state.getTransitionSet().add(new Transition(on("submit"), to("finish")));
new EndState(flow, "finish");
MockRequestControlContext context = new MockRequestControlContext(flow);
state.enter(context);
assertTrue("Render not called", context.getFlowScope().contains("renderCalled"));
assertFalse(context.getFlowExecutionRedirectSent());
}
public void testEnterViewStateWithVariables() {
Flow flow = new Flow("myFlow");
StubViewFactory viewFactory = new StubViewFactory();
ViewState state = new ViewState(flow, "viewState", viewFactory);
state.addVariable(new ViewVariable("foo", new VariableValueFactory() {
public Object createInitialValue(RequestContext context) {
return "bar";
}
public void restoreReferences(Object value, RequestContext context) {
}
}));
MockRequestControlContext context = new MockRequestControlContext(flow);
state.enter(context);
assertEquals("bar", context.getFlowScope().getString("foo"));
assertTrue("Render not called", context.getFlowScope().contains("renderCalled"));
assertFalse(context.getFlowExecutionRedirectSent());
}
public void testEnterViewStateWithLocalRedirect() {
Flow flow = new Flow("myFlow");
StubViewFactory viewFactory = new StubViewFactory();
ViewState state = new ViewState(flow, "viewState", viewFactory);
state.setRedirect(Boolean.TRUE);
state.getTransitionSet().add(new Transition(on("submit"), to("finish")));
new EndState(flow, "finish");
MockRequestControlContext context = new MockRequestControlContext(flow);
state.enter(context);
assertFalse("Render called", context.getFlowScope().contains("renderCalled"));
assertTrue(context.getMockExternalContext().flowExecutionRedirectRequested());
}
public void testEnterViewStateWithNoLocalRedirect() {
Flow flow = new Flow("myFlow");
StubViewFactory viewFactory = new StubViewFactory();
ViewState state = new ViewState(flow, "viewState", viewFactory);
state.setRedirect(Boolean.FALSE);
MockRequestControlContext context = new MockRequestControlContext(flow);
state.enter(context);
assertTrue("Render called", context.getFlowScope().contains("renderCalled"));
assertFalse(context.getMockExternalContext().flowExecutionRedirectRequested());
}
public void testEnterViewStateWithAlwaysRedirectOnPause() {
Flow flow = new Flow("myFlow");
StubViewFactory viewFactory = new StubViewFactory();
ViewState state = new ViewState(flow, "viewState", viewFactory);
state.getTransitionSet().add(new Transition(on("submit"), to("finish")));
new EndState(flow, "finish");
MockRequestControlContext context = new MockRequestControlContext(flow);
context.setAlwaysRedirectOnPause(true);
state.enter(context);
assertFalse("Render called", context.getFlowScope().contains("renderCalled"));
assertTrue(context.getMockExternalContext().flowExecutionRedirectRequested());
}
public void testEnterViewStateWithPopup() {
Flow flow = new Flow("myFlow");
StubViewFactory viewFactory = new StubViewFactory();
ViewState state = new ViewState(flow, "viewState", viewFactory);
state.setPopup(true);
MockRequestControlContext context = new MockRequestControlContext(flow);
context.setAlwaysRedirectOnPause(true);
state.enter(context);

View File

@@ -142,11 +142,55 @@ public class FlowControllerTests extends TestCase {
EasyMock.replay(new Object[] { executor });
ModelAndView mv = controller.handleRequest(request, response);
assertNull(mv);
EasyMock.verify(new Object[] { executor });
assertEquals("/springtravel/app/foo?execution=12345", response.getRedirectedUrl());
EasyMock.verify(new Object[] { executor });
}
public void testLaunchFlowWithExecutionRedirectAjaxHeaderOpenInPopup() throws Exception {
request.setContextPath("/springtravel");
request.setServletPath("/app");
request.setPathInfo("/foo");
request.setRequestURI("/springtravel/app/foo");
request.setMethod("GET");
request.addHeader("Accept", "text/html;type=ajax");
Map parameters = new HashMap();
request.setParameters(parameters);
context.setAjaxRequest(true);
context.requestFlowExecutionRedirect();
context.requestRedirectInPopup();
executor.launchExecution("foo", new LocalAttributeMap(parameters), context);
FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345");
EasyMock.expectLastCall().andReturn(result);
EasyMock.replay(new Object[] { executor });
ModelAndView mv = controller.handleRequest(request, response);
assertNull(mv);
assertEquals(null, response.getRedirectedUrl());
assertEquals("true", response.getHeader("Flow-Modal-View"));
assertEquals("/springtravel/app/foo?execution=12345", response.getHeader("Flow-Redirect-URL"));
EasyMock.verify(new Object[] { executor });
}
public void testLaunchFlowWithExecutionRedirectAjaxParameter() throws Exception {
request.setContextPath("/springtravel");
request.setServletPath("/app");
request.setPathInfo("/foo");
request.setRequestURI("/springtravel/app/foo");
request.setMethod("GET");
request.addParameter("ajaxSource", "this");
context.setAjaxRequest(true);
context.requestFlowExecutionRedirect();
executor.launchExecution("foo", new LocalAttributeMap(request.getParameterMap()), context);
FlowExecutionResult result = FlowExecutionResult.createPausedResult("foo", "12345");
EasyMock.expectLastCall().andReturn(result);
EasyMock.replay(new Object[] { executor });
ModelAndView mv = controller.handleRequest(request, response);
assertNull(mv);
assertEquals(null, response.getRedirectedUrl());
assertEquals(null, response.getHeader("Flow-Modal-View"));
assertEquals("/springtravel/app/foo?execution=12345", response.getHeader("Flow-Redirect-URL"));
EasyMock.verify(new Object[] { executor });
}
public void testLaunchFlowWithDefinitionRedirect() throws Exception {
request.setContextPath("/springtravel");
request.setServletPath("/app");