Moved model validation behavior into the FlowActionListener, to only be invoked if an non-immediate ActionEvent has been signaled.

This commit is contained in:
Jeremy Grelle
2008-04-23 00:02:39 +00:00
parent 72ccde6297
commit 53622691d1
5 changed files with 137 additions and 125 deletions

View File

@@ -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.
* <p>
* This implementation bypasses the JSF {@link NavigationHandler} mechanism to instead let the event be handled directly
* by Web Flow.
* <p>
* 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()) });
}
}
}
}
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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");
}
});
}
}
}

View File

@@ -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");
}
});
}
}
}