view state user event refactoring

This commit is contained in:
Keith Donald
2009-04-13 21:25:47 +00:00
parent 58c0e474bf
commit 8ae6e52d9c
13 changed files with 133 additions and 195 deletions

View File

@@ -97,24 +97,26 @@ public class JsfView implements View {
return requestContext.getRequestParameters().size() > 1;
}
/*
* Executes postback-processing portions of the standard JSF lifecycle including APPLY_REQUEST_VALUES through
* INVOKE_APPLICATION.
*/
public void processUserEvent() {
/*
* Executes postback-processing portions of the standard JSF lifecycle including APPLY_REQUEST_VALUES through
* INVOKE_APPLICATION.
*/
FacesContext facesContext = FlowFacesContext.newInstance(requestContext, facesLifecycle);
facesContext.setViewRoot(viewRoot);
try {
// TODO - render response / response complete check
facesLifecycle.execute(facesContext);
if (!hasFlowEvent()) {
requestContext.getFlashScope().put(ViewRootHolder.VIEW_ROOT_HOLDER_KEY,
new ViewRootHolder(getViewRoot()));
}
} finally {
facesContext.release();
}
}
public Object getUserEventState() {
// TODO - return view root holder
return null;
}
public boolean hasFlowEvent() {
return requestContext.getExternalContext().getRequestMap().contains(EVENT_KEY);
}

View File

@@ -179,12 +179,8 @@ public class ViewState extends TransitionableState {
context.getExternalContext().requestRedirectInPopup();
}
} else {
if (externalContext.isResponseAllowed()) {
View view = viewFactory.getView(context);
render(context, view);
} else {
externalContext.recordResponseComplete();
}
View view = viewFactory.getView(context);
render(context, view);
}
}
}
@@ -193,33 +189,48 @@ public class ViewState extends TransitionableState {
restoreVariables(context);
View view = viewFactory.getView(context);
if (view.userEventQueued()) {
view.processUserEvent();
boolean stateExited = false;
if (view.hasFlowEvent()) {
Event event = view.getFlowEvent();
if (logger.isDebugEnabled()) {
logger.debug("Event '" + event.getId() + "' returned from view " + view);
}
stateExited = context.handleEvent(event);
}
boolean stateExited = handleEvent(view, context);
if (!stateExited) {
ExternalContext externalContext = context.getExternalContext();
if (externalContext.isResponseComplete()) {
clearFlashIfNotRedirecting(context);
} else {
if (externalContext.isAjaxRequest()) {
renderIfAllowed(context, view);
render(context, view);
} else {
if (shouldRedirect(context)) {
context.getFlashScope().put(View.USER_EVENT_STATE_ATTRIBUTE, view.getUserEventState());
externalContext.requestFlowExecutionRedirect();
} else {
renderIfAllowed(context, view);
render(context, view);
}
}
}
}
} else {
renderIfAllowed(context, view);
refresh(view, context);
}
}
private boolean handleEvent(View view, RequestControlContext context) {
view.processUserEvent();
if (view.hasFlowEvent()) {
Event event = view.getFlowEvent();
if (logger.isDebugEnabled()) {
logger.debug("Event '" + event.getId() + "' returned from view " + view);
}
return context.handleEvent(event);
} else {
return false;
}
}
private void refresh(View view, RequestControlContext context) {
ExternalContext externalContext = context.getExternalContext();
if (externalContext.isResponseComplete()) {
clearFlash(context);
} else {
render(context, view);
}
}
@@ -250,15 +261,6 @@ public class ViewState extends TransitionableState {
}
}
private void renderIfAllowed(RequestControlContext context, View view) throws ViewRenderingException {
ExternalContext externalContext = context.getExternalContext();
if (externalContext.isResponseAllowed()) {
render(context, view);
} else {
externalContext.recordResponseComplete();
}
}
private void render(RequestControlContext context, View view) throws ViewRenderingException {
if (logger.isDebugEnabled()) {
logger.debug("Rendering + " + view);

View File

@@ -58,6 +58,8 @@ public class ActionExecutingViewFactory implements ViewFactory {
private String eventId;
private boolean userEventProcessed;
private ActionExecutingView(Action action, RequestContext requestContext) {
this.action = action;
this.requestContext = requestContext;
@@ -70,15 +72,15 @@ public class ActionExecutingViewFactory implements ViewFactory {
}
public boolean userEventQueued() {
determineEventId(requestContext);
return eventId != null;
return getEventId() != null;
}
public void processUserEvent() {
userEventProcessed = true;
}
public boolean hasFlowEvent() {
return eventId != null;
return userEventProcessed && getEventId() != null;
}
public Event getFlowEvent() {
@@ -88,8 +90,19 @@ public class ActionExecutingViewFactory implements ViewFactory {
return new Event(this, eventId);
}
private void determineEventId(RequestContext context) {
eventId = WebUtils.findParameterValue(context.getRequestParameters().asMap(), "_eventId");
public Object getUserEventState() {
return null;
}
private String getEventId() {
if (eventId == null) {
eventId = determineEventId(requestContext);
}
return this.eventId;
}
protected String determineEventId(RequestContext context) {
return WebUtils.findParameterValue(context.getRequestParameters().asMap(), "_eventId");
}
}
}

View File

@@ -31,6 +31,11 @@ public interface View {
*/
public static final String RENDER_FRAGMENTS_ATTRIBUTE = "flowRenderFragments";
/**
* Well-known attribute name for storing the results of processing a user event
*/
public static final String USER_EVENT_STATE_ATTRIBUTE = "viewUserEventState";
/**
* Render this view's content.
* @throws IOException if an IO Exception occured rendering the view
@@ -45,7 +50,7 @@ public interface View {
/**
* Process the queued user event. Should only be called when {@link #userEventQueued()} returns true. After calling
* this method, a flow event may be queued that should be raised in the Web Flow system.
* this method, a flow event may be raised that should be handled in the Web Flow system.
* @see #hasFlowEvent()
*/
public void processUserEvent();
@@ -64,4 +69,12 @@ public interface View {
*/
public Event getFlowEvent();
/**
* A memento holding the results of processing a user event. Used to allow transient view state such as binding and
* validation errors to survive a flow execution redirect.
* @return the user event state object, or null if no event state needs managing
* @see #processUserEvent()
*/
public Object getUserEventState();
}

View File

@@ -89,6 +89,8 @@ public abstract class AbstractMvcView implements View {
private MessageCodesResolver messageCodesResolver;
private boolean userEventProcessed;
/**
* Creates a new MVC view.
* @param view the Spring MVC view to render
@@ -185,52 +187,56 @@ public abstract class AbstractMvcView implements View {
}
public boolean userEventQueued() {
eventId = determineEventId(requestContext);
return eventId != null;
return getEventId() != null;
}
public void processUserEvent() {
if (logger.isDebugEnabled()) {
logger.debug("User event '" + eventId + "' raised");
}
Object model = getModelObject();
if (model == null) {
if (logger.isDebugEnabled()) {
logger.debug("No model to bind to; done processing user event");
}
String eventId = getEventId();
if (eventId == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Resolved model " + model);
logger.debug("Processing user event '" + eventId + "'");
}
TransitionDefinition transition = requestContext.getMatchingTransition(eventId);
if (shouldBind(model, transition)) {
mappingResults = bind(model);
if (hasErrors(mappingResults)) {
if (logger.isDebugEnabled()) {
logger.debug("Model binding resulted in errors; adding error messages to context");
Object model = getModelObject();
if (model != null) {
if (logger.isDebugEnabled()) {
logger.debug("Resolved model " + model);
}
TransitionDefinition transition = requestContext.getMatchingTransition(eventId);
if (shouldBind(model, transition)) {
mappingResults = bind(model);
if (hasErrors(mappingResults)) {
if (logger.isDebugEnabled()) {
logger.debug("Model binding resulted in errors; adding error messages to context");
}
addErrorMessages(mappingResults);
}
if (shouldValidate(model, transition)) {
validate(model);
}
addErrorMessages(mappingResults);
}
if (shouldValidate(model, transition)) {
validate(model);
} else {
if (logger.isDebugEnabled()) {
logger.debug("No model to bind to; done processing user event");
}
}
if (mappingResults != null && hasErrors(mappingResults)) {
requestContext.getFlashScope().put(ViewActionStateHolder.KEY,
new ViewActionStateHolder(eventId, mappingResults));
}
userEventProcessed = true;
}
public boolean hasFlowEvent() {
return eventId != null && !requestContext.getMessageContext().hasErrorMessages();
return userEventProcessed && !requestContext.getMessageContext().hasErrorMessages();
}
public Event getFlowEvent() {
if (!hasFlowEvent()) {
return null;
}
return new Event(this, eventId, requestContext.getRequestParameters().asAttributeMap());
return new Event(this, getEventId(), requestContext.getRequestParameters().asAttributeMap());
}
public Object getUserEventState() {
return new ViewActionStateHolder(eventId, mappingResults);
}
public String toString() {
@@ -262,22 +268,15 @@ public abstract class AbstractMvcView implements View {
*/
protected abstract void doRender(Map model) throws Exception;
/**
* Obtain the user event from the current flow request. The default implementation returns the value of the request
* parameter with name {@link #setEventIdParameterName(String) eventIdParameterName}. Subclasses may override.
* @param context the current flow request context
* @return the user event that occurred
*/
protected String determineEventId(RequestContext context) {
return WebUtils.findParameterValue(context.getRequestParameters().asMap(), eventIdParameterName);
}
/**
* Returns the id of the user event being processed.
* @return the user event
*/
protected String getEventId() {
return eventId;
if (eventId == null) {
eventId = determineEventId(requestContext);
}
return this.eventId;
}
/**
@@ -303,6 +302,16 @@ public abstract class AbstractMvcView implements View {
return mappingResults;
}
/**
* Obtain the user event from the current flow request. The default implementation returns the value of the request
* parameter with name {@link #setEventIdParameterName(String) eventIdParameterName}. Subclasses may override.
* @param context the current flow request context
* @return the user event that occurred
*/
protected String determineEventId(RequestContext context) {
return WebUtils.findParameterValue(context.getRequestParameters().asMap(), eventIdParameterName);
}
// package private
/**

View File

@@ -90,7 +90,7 @@ public abstract class AbstractMvcViewFactory implements ViewFactory {
mvcView.setFieldMarkerPrefix(fieldMarkerPrefix);
}
ViewActionStateHolder stateHolder = (ViewActionStateHolder) context.getFlashScope().get(
ViewActionStateHolder.KEY);
View.USER_EVENT_STATE_ATTRIBUTE);
if (stateHolder != null) {
mvcView.restoreState(stateHolder);
}

View File

@@ -174,20 +174,17 @@ public class MockExternalContext implements ExternalContext {
}
public void requestFlowExecutionRedirect() throws IllegalStateException {
assertResponseAllowed();
flowExecutionRedirectRequested = true;
recordResponseComplete();
}
public void requestFlowDefinitionRedirect(String flowId, MutableAttributeMap input) throws IllegalStateException {
assertResponseAllowed();
flowDefinitionRedirectFlowId = flowId;
flowDefinitionRedirectFlowInput = input;
recordResponseComplete();
}
public void requestExternalRedirect(String uri) throws IllegalStateException {
assertResponseAllowed();
externalRedirectUrl = uri;
recordResponseComplete();
}
@@ -435,8 +432,7 @@ public class MockExternalContext implements ExternalContext {
throw new IllegalStateException(
"A response is not allowed because an externalRedirect has already been requested on this ExternalContext");
}
throw new IllegalStateException(
"A response is not allowed because one has already been completed on this ExternalContext");
throw new IllegalStateException("A response is not allowed");
}
}

View File

@@ -118,6 +118,9 @@ public class MockRequestControlContext extends MockRequestContext implements Req
}
public boolean getRedirectOnPause() {
if (!getExternalContext().isResponseAllowed()) {
return true;
}
Boolean redirectOnPause = getMockFlowExecutionContext().getAttributes().getBoolean("alwaysRedirectOnPause");
return redirectOnPause != null ? redirectOnPause.booleanValue() : false;
}

View File

@@ -111,6 +111,10 @@ class MockViewFactoryCreator implements ViewFactoryCreator {
return new Event(this, context.getRequestParameters().get("_eventId"));
}
public Object getUserEventState() {
return null;
}
public void render() throws IOException {
context.getExternalContext().getResponseWriter().write(viewId);
}

View File

@@ -45,6 +45,10 @@ public class StubViewFactory implements ViewFactory {
}
public Object getUserEventState() {
return null;
}
public boolean hasFlowEvent() {
return context.getExternalContext().getRequestParameterMap().contains("_eventId");
}
@@ -52,5 +56,6 @@ public class StubViewFactory implements ViewFactory {
public Event getFlowEvent() {
return new Event(this, context.getExternalContext().getRequestParameterMap().get("_eventId"));
}
}
}

View File

@@ -52,7 +52,7 @@ public class ViewStateTests extends TestCase {
state.enter(context);
assertFalse("Render called", context.getFlowScope().contains("renderCalled"));
assertTrue(context.getExternalContext().isResponseComplete());
assertFalse(context.getMockExternalContext().getFlowExecutionRedirectRequested());
assertTrue(context.getMockExternalContext().getFlowExecutionRedirectRequested());
}
public void testEnterViewStateResponseAlreadyComplete() {
@@ -160,22 +160,6 @@ public class ViewStateTests extends TestCase {
assertFalse(context.getMockExternalContext().getFlowExecutionRedirectRequested());
}
public void testResumeViewStateForRefreshResponseNotAllowed() {
Flow flow = new Flow("myFlow");
StubViewFactory viewFactory = new StubViewFactory();
ViewState state = new ViewState(flow, "viewState", viewFactory);
MockRequestControlContext context = new MockRequestControlContext(flow);
state.enter(context);
assertTrue("Render called", context.getFlowScope().contains("renderCalled"));
context = new MockRequestControlContext(context.getFlowExecutionContext());
context.getMockExternalContext().setResponseAllowed(false);
context.getFlowScope().remove("renderCalled");
state.resume(context);
assertFalse("Render called", context.getFlowScope().contains("renderCalled"));
assertTrue(context.getExternalContext().isResponseComplete());
assertFalse(context.getMockExternalContext().getFlowExecutionRedirectRequested());
}
public void testResumeViewStateForRefreshResponseCompleteRecorded() {
Flow flow = new Flow("myFlow");
StubViewFactory viewFactory = new StubViewFactory();
@@ -269,7 +253,7 @@ public class ViewStateTests extends TestCase {
assertTrue(context.getFlowExecutionContext().isActive());
assertTrue(context.getExternalContext().isResponseComplete());
assertFalse("Render called", context.getFlowScope().contains("renderCalled"));
assertFalse(context.getMockExternalContext().getFlowExecutionRedirectRequested());
assertTrue(context.getMockExternalContext().getFlowExecutionRedirectRequested());
}
public void testResumeViewStateForEventStateNotExitedNonAjaxRedirectEnabled() {
@@ -315,30 +299,6 @@ public class ViewStateTests extends TestCase {
assertFalse(context.getMockExternalContext().getFlowExecutionRedirectRequested());
}
public void testResumeViewStateForEventStateNotExitedAjaxResponseNotAllowed() {
Flow flow = new Flow("myFlow");
StubViewFactory viewFactory = new StubViewFactory();
ViewState state = new ViewState(flow, "viewState", viewFactory);
Transition t = new Transition(on("submit"), null);
TestAction action = new TestAction();
t.setExecutionCriteria(new ActionTransitionCriteria(action));
state.getTransitionSet().add(t);
MockRequestControlContext context = new MockRequestControlContext(flow);
context.getMockExternalContext().setAjaxRequest(true);
state.enter(context);
context = new MockRequestControlContext(context.getFlowExecutionContext());
context.putRequestParameter("_eventId", "submit");
context.getMockExternalContext().setAjaxRequest(true);
context.getMockExternalContext().setResponseAllowed(false);
context.getFlowScope().remove("renderCalled");
state.resume(context);
assertTrue(context.getFlowExecutionContext().isActive());
assertEquals(1, action.getExecutionCount());
assertTrue(context.getExternalContext().isResponseComplete());
assertFalse("Render called", context.getFlowScope().contains("renderCalled"));
assertFalse(context.getMockExternalContext().getFlowExecutionRedirectRequested());
}
public void testResumeViewStateForEventStateNoExitActionRecordedResponseComplete() {
Flow flow = new Flow("myFlow");
StubViewFactory viewFactory = new StubViewFactory();

View File

@@ -16,7 +16,6 @@ import org.springframework.mock.web.portlet.MockRenderResponse;
import org.springframework.web.servlet.ViewRendererServlet;
import org.springframework.webflow.expression.DefaultExpressionParserFactory;
import org.springframework.webflow.mvc.view.AbstractMvcView;
import org.springframework.webflow.mvc.view.ViewActionStateHolder;
import org.springframework.webflow.mvc.view.MvcViewTests.BindBean;
import org.springframework.webflow.test.MockFlowExecutionKey;
import org.springframework.webflow.test.MockRequestContext;
@@ -61,9 +60,6 @@ public class PortletMvcViewTests extends TestCase {
view.setMessageCodesResolver(new WebFlowMessageCodesResolver());
view.processUserEvent();
assertEquals(true, bindBean.getBooleanProperty());
ViewActionStateHolder holder = (ViewActionStateHolder) context.getFlashScope().get(ViewActionStateHolder.KEY);
assertEquals("submit", holder.getEventId());
assertNotNull(holder.getMappingResults());
}
}

View File

@@ -228,71 +228,6 @@ public class MvcViewTests extends TestCase {
context.setAlwaysRedirectOnPause(true);
view.processUserEvent();
assertFalse(view.hasFlowEvent());
assertTrue(context.getFlashScope().contains(ViewActionStateHolder.KEY));
assertTrue(context.getExternalContext().isResponseCompleteFlowExecutionRedirect());
view.render();
assertEquals(context.getFlowScope().get("bindBean"), model.get("bindBean"));
BindingModel bm = (BindingModel) model.get(BindingResult.MODEL_KEY_PREFIX + "bindBean");
assertNotNull(bm);
assertEquals("bogus 1", bm.getFieldValue("integerProperty"));
assertEquals("bogus 2", bm.getFieldValue("dateProperty"));
}
public void testResumeEventBindingErrorsNoRedirectAllowed() throws Exception {
MockRequestControlContext context = new MockRequestControlContext();
context.putRequestParameter("_eventId", "submit");
context.putRequestParameter("integerProperty", "bogus 1");
context.putRequestParameter("dateProperty", "bogus 2");
BindBean bindBean = new BindBean();
StaticExpression modelObject = new StaticExpression(bindBean);
modelObject.setExpressionString("bindBean");
context.getCurrentState().getAttributes().put("model", modelObject);
context.getFlowScope().put("bindBean", bindBean);
context.getMockExternalContext().setNativeContext(new MockServletContext());
context.getMockExternalContext().setNativeRequest(new MockHttpServletRequest());
context.getMockExternalContext().setNativeResponse(new MockHttpServletResponse());
context.getMockFlowExecutionContext().setKey(new MockFlowExecutionKey("c1v1"));
org.springframework.web.servlet.View mvcView = new MockView();
AbstractMvcView view = new MockMvcView(mvcView, context);
view.setExpressionParser(DefaultExpressionParserFactory.getExpressionParser());
view.setMessageCodesResolver(new WebFlowMessageCodesResolver());
view.processUserEvent();
assertTrue(context.getFlashScope().contains(ViewActionStateHolder.KEY));
assertFalse(view.hasFlowEvent());
assertFalse(context.getExternalContext().isResponseComplete());
assertFalse(context.getExternalContext().isResponseCompleteFlowExecutionRedirect());
view.render();
assertEquals(context.getFlowScope().get("bindBean"), model.get("bindBean"));
BindingModel bm = (BindingModel) model.get(BindingResult.MODEL_KEY_PREFIX + "bindBean");
assertNotNull(bm);
assertEquals("bogus 1", bm.getFieldValue("integerProperty"));
assertEquals("bogus 2", bm.getFieldValue("dateProperty"));
}
public void testResumeEventBindingErrorsNoRedirectAjaxRequest() throws Exception {
MockRequestControlContext context = new MockRequestControlContext();
context.putRequestParameter("_eventId", "submit");
context.putRequestParameter("integerProperty", "bogus 1");
context.putRequestParameter("dateProperty", "bogus 2");
BindBean bindBean = new BindBean();
StaticExpression modelObject = new StaticExpression(bindBean);
modelObject.setExpressionString("bindBean");
context.getCurrentState().getAttributes().put("model", modelObject);
context.getFlowScope().put("bindBean", bindBean);
context.getMockExternalContext().setNativeContext(new MockServletContext());
context.getMockExternalContext().setNativeRequest(new MockHttpServletRequest());
context.getMockExternalContext().setNativeResponse(new MockHttpServletResponse());
context.getMockFlowExecutionContext().setKey(new MockFlowExecutionKey("c1v1"));
org.springframework.web.servlet.View mvcView = new MockView();
AbstractMvcView view = new MockMvcView(mvcView, context);
view.setExpressionParser(DefaultExpressionParserFactory.getExpressionParser());
view.setMessageCodesResolver(new WebFlowMessageCodesResolver());
context.getMockExternalContext().setAjaxRequest(true);
view.processUserEvent();
assertFalse(view.hasFlowEvent());
assertFalse(context.getExternalContext().isResponseComplete());
assertFalse(context.getExternalContext().isResponseCompleteFlowExecutionRedirect());
assertTrue(context.getFlashScope().contains(ViewActionStateHolder.KEY));
view.render();
assertEquals(context.getFlowScope().get("bindBean"), model.get("bindBean"));
BindingModel bm = (BindingModel) model.get(BindingResult.MODEL_KEY_PREFIX + "bindBean");