diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowActionListener.java b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowActionListener.java index 9212d74a..0123d3e9 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowActionListener.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowActionListener.java @@ -15,6 +15,10 @@ */ package org.springframework.faces.webflow; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + import javax.faces.application.NavigationHandler; import javax.faces.component.ActionSource; import javax.faces.context.FacesContext; @@ -24,13 +28,27 @@ import javax.faces.event.ActionListener; 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.RequestContext; +import org.springframework.webflow.execution.RequestContextHolder; +import org.springframework.webflow.execution.View; +import org.springframework.webflow.mvc.view.MessageContextErrors; /** * The default {@link ActionListener} implementation to be used with Web Flow. *
* This implementation bypasses the JSF {@link NavigationHandler} mechanism to instead let the event be handled directly * by Web Flow. + *
+ * Web Flow's model-level validation will be invoked here after an event has been detected if the event is not an + * immediate event. * * @author Jeremy Grelle */ @@ -38,6 +56,8 @@ public class FlowActionListener implements ActionListener { private static final Log logger = LogFactory.getLog(FlowActionListener.class); + private static final String MESSAGES_ID = "messages"; + private ActionListener delegate; public FlowActionListener(ActionListener delegate) { @@ -51,18 +71,20 @@ public class FlowActionListener implements ActionListener { } FacesContext context = FacesContext.getCurrentInstance(); ActionSource source = (ActionSource) actionEvent.getSource(); - String result = null; + String eventId = null; if (source.getAction() != null) { if (logger.isDebugEnabled()) { logger.debug("Invoking action " + source.getAction()); } - result = (String) source.getAction().invoke(context, null); + eventId = (String) source.getAction().invoke(context, null); } - if (StringUtils.hasText(result)) { + if (StringUtils.hasText(eventId)) { if (logger.isDebugEnabled()) { - logger.debug("Event '" + result + "' detected"); + logger.debug("Event '" + eventId + "' detected"); + } + if (source.isImmediate() || validateModel(context, eventId)) { + context.getExternalContext().getRequestMap().put(JsfView.EVENT_KEY, eventId); } - context.getExternalContext().getRequestMap().put(JsfView.EVENT_KEY, result); } else { logger.debug("No action event detected"); context.getExternalContext().getRequestMap().remove(JsfView.EVENT_KEY); @@ -71,4 +93,92 @@ public class FlowActionListener implements ActionListener { // required in the case of this action listener firing immediately (immediate=true) before validation context.renderResponse(); } + + // internal helpers + + private boolean validateModel(FacesContext facesContext, String eventId) { + boolean isValid = true; + RequestContext requestContext = RequestContextHolder.getRequestContext(); + Object model = getModelObject(requestContext); + if (shouldValidate(requestContext, model, eventId)) { + validate(requestContext, model); + if (requestContext.getMessageContext().hasErrorMessages()) { + isValid = false; + if (requestContext.getExternalContext().isAjaxRequest()) { + List fragments = new ArrayList(); + String formId = getModelExpression(requestContext).getExpressionString(); + if (facesContext.getViewRoot().findComponent(formId) != null) { + fragments.add(formId); + } + if (facesContext.getViewRoot().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); + } + } + } + } + return isValid; + } + + private Object getModelObject(RequestContext requestContext) { + Expression model = getModelExpression(requestContext); + if (model != null) { + return model.getValue(requestContext); + } else { + return null; + } + } + + private Expression getModelExpression(RequestContext requestContext) { + return (Expression) requestContext.getCurrentState().getAttributes().get("model"); + } + + private boolean shouldValidate(RequestContext requestContext, Object model, String eventId) { + if (model == null) { + return false; + } + TransitionableStateDefinition currentState = (TransitionableStateDefinition) requestContext.getCurrentState(); + TransitionDefinition transition = currentState.getTransition(eventId); + if (transition != null) { + if (transition.getAttributes().contains("bind")) { + return transition.getAttributes().getBoolean("bind").booleanValue(); + } + } + return true; + } + + private void validate(RequestContext requestContext, 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(requestContext).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()) }); + } + } + } + } + } } diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowLifecycle.java b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowLifecycle.java index 420c1c7e..3b215c90 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowLifecycle.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowLifecycle.java @@ -15,6 +15,8 @@ */ package org.springframework.faces.webflow; +import java.io.IOException; + import javax.faces.FacesException; import javax.faces.FactoryFinder; import javax.faces.context.FacesContext; @@ -68,6 +70,7 @@ public class FlowLifecycle extends Lifecycle { /** * Delegates to the wrapped {@link Lifecycle}. + * @throws IOException */ public void render(FacesContext context) throws FacesException { delegate.render(context); 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 f5db79cb..98524daf 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,9 +16,6 @@ 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.component.UIViewRoot; import javax.faces.context.FacesContext; @@ -27,18 +24,9 @@ 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. @@ -51,8 +39,6 @@ public class JsfView implements View { public static final String EVENT_KEY = "org.springframework.webflow.FacesEvent"; - private static final String MESSAGES_ID = "messages"; - private UIViewRoot viewRoot; private Lifecycle facesLifecycle; @@ -94,7 +80,7 @@ public class JsfView implements View { public void render() throws IOException { FacesContext facesContext = FlowFacesContext.newInstance(requestContext, facesLifecycle); facesContext.setViewRoot(viewRoot); - facesContext.renderResponse(); + // TODO move renderResponse behavior into lifecycle??? try { JsfUtils.notifyBeforeListeners(PhaseId.RENDER_RESPONSE, facesLifecycle, facesContext); logger.debug("Asking view handler to render view"); @@ -113,8 +99,6 @@ public class JsfView implements View { try { if (restored && !facesContext.getRenderResponse() && !facesContext.getResponseComplete()) { facesLifecycle.execute(facesContext); - // TODO move this and renderResponse behavior into lifecycle - validateModel(facesContext); } } finally { facesContext.release(); @@ -135,89 +119,6 @@ public class JsfView implements View { // internal helpers - 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); } diff --git a/spring-faces/src/test/java/org/springframework/faces/webflow/FlowActionListenerTests.java b/spring-faces/src/test/java/org/springframework/faces/webflow/FlowActionListenerTests.java index 6be2d885..98d3a3cf 100644 --- a/spring-faces/src/test/java/org/springframework/faces/webflow/FlowActionListenerTests.java +++ b/spring-faces/src/test/java/org/springframework/faces/webflow/FlowActionListenerTests.java @@ -12,8 +12,12 @@ import junit.framework.TestCase; import org.easymock.EasyMock; import org.springframework.webflow.core.collection.AttributeMap; import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.ViewState; import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.RequestContextHolder; +import org.springframework.webflow.execution.View; +import org.springframework.webflow.execution.ViewFactory; public class FlowActionListenerTests extends TestCase { @@ -30,6 +34,7 @@ public class FlowActionListenerTests extends TestCase { RequestContextHolder.setRequestContext(context); AttributeMap flash = new LocalAttributeMap(); EasyMock.expect(context.getFlashScope()).andStubReturn(flash); + EasyMock.expect(context.getCurrentState()).andStubReturn(new MockViewState()); EasyMock.replay(new Object[] { context }); } @@ -84,4 +89,17 @@ public class FlowActionListenerTests extends TestCase { } } + + private class MockViewState extends ViewState { + + public MockViewState() { + super(new Flow("mockFlow"), "mockView", new ViewFactory() { + + public View getView(RequestContext context) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Auto-generated method stub"); + } + }); + } + } } 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 52850e71..6471be55 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,14 +22,10 @@ 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 { @@ -145,7 +141,6 @@ public class JsfViewTests extends TestCase { Boolean.FALSE); EasyMock.expect(flashScope.put(EasyMock.matches(FlowFacesContext.RENDER_RESPONSE_KEY), EasyMock.anyObject())) .andStubReturn(null); - EasyMock.expect(context.getCurrentState()).andStubReturn(new MockViewState()); Lifecycle lifecycle = new NoEventLifecycle(jsfMock.lifecycle()); @@ -175,7 +170,6 @@ public class JsfViewTests extends TestCase { Boolean.FALSE); EasyMock.expect(flashScope.put(EasyMock.matches(FlowFacesContext.RENDER_RESPONSE_KEY), EasyMock.anyObject())) .andStubReturn(null); - EasyMock.expect(context.getCurrentState()).andStubReturn(new MockViewState()); Lifecycle lifecycle = new NoEventLifecycle(jsfMock.lifecycle()); @@ -205,7 +199,6 @@ public class JsfViewTests extends TestCase { Boolean.FALSE); EasyMock.expect(flashScope.put(EasyMock.matches(FlowFacesContext.RENDER_RESPONSE_KEY), EasyMock.anyObject())) .andStubReturn(null); - EasyMock.expect(context.getCurrentState()).andStubReturn(new MockViewState()); Lifecycle lifecycle = new EventSignalingLifecycle(jsfMock.lifecycle()); @@ -300,17 +293,4 @@ 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"); - } - }); - } - } }