From e2567425c0679961a975251d327a67015339f261 Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Wed, 16 Apr 2008 21:34:52 +0000 Subject: [PATCH] SWF-583 - View-state programming model needs to be consistent between MVC and JSF --- .../faces/webflow/JsfView.java | 118 ++++++++++++++++-- .../faces/webflow/JsfViewTests.java | 20 +++ .../webapp/WEB-INF/flows/booking/booking.xml | 7 +- .../flows/booking/enterBookingDetails.xhtml | 6 +- 4 files changed, 135 insertions(+), 16 deletions(-) diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java index 7c91a505..04c102d5 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java @@ -16,6 +16,9 @@ package org.springframework.faces.webflow; import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import javax.faces.FacesException; import javax.faces.component.UIViewRoot; @@ -25,9 +28,18 @@ import javax.faces.lifecycle.Lifecycle; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.message.MessageContext; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.webflow.definition.TransitionDefinition; +import org.springframework.webflow.definition.TransitionableStateDefinition; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.View; +import org.springframework.webflow.mvc.view.MessageContextErrors; /** * JSF-specific {@link View} implementation. @@ -40,6 +52,8 @@ public class JsfView implements View { public static final String EVENT_KEY = "org.springframework.webflow.FacesEvent"; + private static final String MESSAGES_ID = "messages"; + /** * The root of the JSF component tree managed by this view */ @@ -47,24 +61,26 @@ public class JsfView implements View { private Lifecycle facesLifecycle; - private RequestContext context; + private RequestContext requestContext; private String viewId; private boolean restored = false; + private boolean viewErrors = false; + public JsfView(UIViewRoot viewRoot, Lifecycle facesLifecycle, RequestContext context) { this.viewRoot = viewRoot; this.viewId = viewRoot.getViewId(); this.facesLifecycle = facesLifecycle; - this.context = context; + this.requestContext = context; } /** * This implementation performs the standard duties of the JSF RENDER_RESPONSE phase. */ public void render() throws IOException { - FacesContext facesContext = FlowFacesContext.newInstance(context, facesLifecycle); + FacesContext facesContext = FlowFacesContext.newInstance(requestContext, facesLifecycle); facesContext.setViewRoot(viewRoot); facesContext.renderResponse(); try { @@ -82,26 +98,25 @@ public class JsfView implements View { } public void processUserEvent() { - FacesContext facesContext = FlowFacesContext.newInstance(context, facesLifecycle); + FacesContext facesContext = FlowFacesContext.newInstance(requestContext, facesLifecycle); facesContext.setViewRoot(viewRoot); try { if (restored && !facesContext.getResponseComplete() && !facesContext.getRenderResponse()) { facesLifecycle.execute(facesContext); + validateModel(facesContext); facesContext.renderResponse(); } } finally { facesContext.release(); } - } public boolean hasFlowEvent() { - return context.getExternalContext().getRequestMap().contains(EVENT_KEY); + return requestContext.getExternalContext().getRequestMap().contains(EVENT_KEY) && !viewErrors; } public Event getFlowEvent() { - String eventId = (String) context.getExternalContext().getRequestMap().get(EVENT_KEY); - return new Event(this, eventId); + return new Event(this, getEventId()); } public UIViewRoot getViewRoot() { @@ -115,4 +130,91 @@ public class JsfView implements View { public void setRestored(boolean restored) { this.restored = restored; } + + private void validateModel(FacesContext facesContext) { + Object model = getModelObject(); + if (shouldValidate(model)) { + validate(model); + if (requestContext.getMessageContext().hasErrorMessages()) { + viewErrors = true; + if (requestContext.getExternalContext().isAjaxRequest()) { + List fragments = new ArrayList(); + String formId = getModelExpression().getExpressionString(); + if (viewRoot.findComponent(formId) != null) { + fragments.add(formId); + } + if (viewRoot.findComponent(MESSAGES_ID) != null) { + fragments.add(MESSAGES_ID); + } + if (fragments.size() > 0) { + String[] fragmentsArray = new String[fragments.size()]; + for (int i = 0; i < fragments.size(); i++) { + fragmentsArray[i] = (String) fragments.get(i); + } + requestContext.getFlashScope().put(View.RENDER_FRAGMENTS_ATTRIBUTE, fragmentsArray); + } + } + } + } + } + + private Object getModelObject() { + Expression model = getModelExpression(); + if (model != null) { + return model.getValue(requestContext); + } else { + return null; + } + } + + private Expression getModelExpression() { + return (Expression) requestContext.getCurrentState().getAttributes().get("model"); + } + + private boolean shouldValidate(Object model) { + if (model == null) { + return false; + } + TransitionableStateDefinition currentState = (TransitionableStateDefinition) requestContext.getCurrentState(); + TransitionDefinition transition = currentState.getTransition(getEventId()); + if (transition != null) { + if (transition.getAttributes().contains("bind")) { + return transition.getAttributes().getBoolean("bind").booleanValue(); + } + } + return true; + } + + private void validate(Object model) { + String validateMethodName = "validate" + StringUtils.capitalize(requestContext.getCurrentState().getId()); + Method validateMethod = ReflectionUtils.findMethod(model.getClass(), validateMethodName, + new Class[] { MessageContext.class }); + if (validateMethod != null) { + ReflectionUtils.invokeMethod(validateMethod, model, new Object[] { requestContext.getMessageContext() }); + } + BeanFactory beanFactory = requestContext.getActiveFlow().getApplicationContext(); + if (beanFactory != null) { + String validatorName = getModelExpression().getExpressionString() + "Validator"; + if (beanFactory.containsBean(validatorName)) { + Object validator = beanFactory.getBean(validatorName); + validateMethod = ReflectionUtils.findMethod(validator.getClass(), validateMethodName, new Class[] { + model.getClass(), MessageContext.class }); + if (validateMethod != null) { + ReflectionUtils.invokeMethod(validateMethod, validator, new Object[] { model, + requestContext.getMessageContext() }); + } else { + validateMethod = ReflectionUtils.findMethod(validator.getClass(), validateMethodName, new Class[] { + model.getClass(), Errors.class }); + if (validateMethod != null) { + ReflectionUtils.invokeMethod(validateMethod, validator, new Object[] { model, + new MessageContextErrors(requestContext.getMessageContext()) }); + } + } + } + } + } + + private String getEventId() { + return (String) requestContext.getExternalContext().getRequestMap().get(EVENT_KEY); + } } \ No newline at end of file diff --git a/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java b/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java index dba4fa9e..486bca63 100644 --- a/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java +++ b/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java @@ -22,10 +22,14 @@ import org.springframework.webflow.core.collection.LocalAttributeMap; import org.springframework.webflow.core.collection.MutableAttributeMap; import org.springframework.webflow.definition.FlowDefinition; import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.ViewState; import org.springframework.webflow.execution.FlowExecutionContext; import org.springframework.webflow.execution.FlowExecutionKey; import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.RequestContextHolder; +import org.springframework.webflow.execution.View; +import org.springframework.webflow.execution.ViewFactory; import org.springframework.webflow.test.MockExternalContext; public class JsfViewTests extends TestCase { @@ -141,6 +145,7 @@ public class JsfViewTests extends TestCase { Boolean.FALSE); EasyMock.expect(flashMap.put(EasyMock.matches(FlowFacesContext.RENDER_RESPONSE_KEY), EasyMock.anyObject())) .andStubReturn(null); + EasyMock.expect(context.getCurrentState()).andStubReturn(new MockViewState()); Lifecycle lifecycle = new NoEventLifecycle(jsfMock.lifecycle()); @@ -170,6 +175,7 @@ public class JsfViewTests extends TestCase { Boolean.FALSE); EasyMock.expect(flashMap.put(EasyMock.matches(FlowFacesContext.RENDER_RESPONSE_KEY), EasyMock.anyObject())) .andStubReturn(null); + EasyMock.expect(context.getCurrentState()).andStubReturn(new MockViewState()); Lifecycle lifecycle = new NoEventLifecycle(jsfMock.lifecycle()); @@ -199,6 +205,7 @@ public class JsfViewTests extends TestCase { Boolean.FALSE); EasyMock.expect(flashMap.put(EasyMock.matches(FlowFacesContext.RENDER_RESPONSE_KEY), EasyMock.anyObject())) .andStubReturn(null); + EasyMock.expect(context.getCurrentState()).andStubReturn(new MockViewState()); Lifecycle lifecycle = new EventSignalingLifecycle(jsfMock.lifecycle()); @@ -293,4 +300,17 @@ public class JsfViewTests extends TestCase { throw new UnsupportedOperationException("Auto-generated method stub"); } } + + private class MockViewState extends ViewState { + + public MockViewState() { + super(new Flow("mockFlow"), VIEW_ID, new ViewFactory() { + + public View getView(RequestContext context) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Auto-generated method stub"); + } + }); + } + } } diff --git a/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/flows/booking/booking.xml b/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/flows/booking/booking.xml index 66093b27..ab3094d5 100755 --- a/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/flows/booking/booking.xml +++ b/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/flows/booking/booking.xml @@ -13,11 +13,8 @@ - - - - - + + diff --git a/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/flows/booking/enterBookingDetails.xhtml b/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/flows/booking/enterBookingDetails.xhtml index e6062e0b..2e3ebfda 100644 --- a/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/flows/booking/enterBookingDetails.xhtml +++ b/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/flows/booking/enterBookingDetails.xhtml @@ -10,12 +10,12 @@

Book Hotel

- -
+ +
- +
Name: