diff --git a/build-spring-webflow/resources/changelog.txt b/build-spring-webflow/resources/changelog.txt index 74f07737..c915632e 100644 --- a/build-spring-webflow/resources/changelog.txt +++ b/build-spring-webflow/resources/changelog.txt @@ -11,6 +11,12 @@ Bug Fixes * Fixed bug where Web Flow upgrader tool did not properly output end-state 'output' elements (SWF-969) * Fixed bug where String to Enum conversions failed to work in conjunction with BeanWrapperExpressionParser, enabled when setting "useSpringBinding" flag on a MvcViewFactoryCreator to true (SWF-1005). * Fixed bug where Collection and Array property bindings did not work with custom converters (SWF-984) +* Fixed bug where form:checkbox and form:checkboxes tags did not apply type conversion properly in views rendered by Web Flows (SWF-986) +* Fixed bug where Errors nestedPath attribute was not respected in views rendered by Web Flow (SWF-973) +* Fixed bug where Errors nestedPath attribute was not respected when using Errors API programatically within a Validator called by Web Flow (SWF-973) + +Improvements +* Improved FlowExecution test documentation (SWF-???, SWF-???) Changes in version 2.0.5 (14.11.2008) ------------------------------------- diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java index d24bd751..d8679afd 100644 --- a/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java +++ b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java @@ -36,6 +36,17 @@ public interface ConversionService { */ public Object executeConversion(Object source, Class targetClass) throws ConversionException; + /** + * Execute a conversion using the custom converter with the provided id. + * @param converterId the id of the custom converter, which must be registered with this conversion service and + * capable of converting to the target class + * @param source the source to convert from (may be null) + * @param targetClass the target class to convert to + * @return the converted object, an instance of the targetClass + * @throws ConversionException if an exception occurred during the conversion process + */ + public Object executeConversion(String converterId, Object source, Class targetClass); + /** * Return the default conversion executor capable of converting source objects of the specified * sourceClass to instances of the targetClass. @@ -65,8 +76,8 @@ public interface ConversionService { /** * Return all conversion executors capable of converting from the provided sourceClass. For - * example, getConversionExecutor(String.class) would return all converters that convert from String - * to some other Object. Mainly useful for adapting a set of converters to some other environment. + * example, getConversionExecutor(String.class) would return all converters that convert from String to + * some other Object. Mainly useful for adapting a set of converters to some other environment. * @param sourceClass the source class converting from * @return the conversion executors that can convert from that source class */ @@ -78,4 +89,5 @@ public interface ConversionService { * @return the class, or null if no alias exists */ public Class getClassForAlias(String alias); + } \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/service/GenericConversionService.java b/spring-binding/src/main/java/org/springframework/binding/convert/service/GenericConversionService.java index d220000c..28139444 100644 --- a/spring-binding/src/main/java/org/springframework/binding/convert/service/GenericConversionService.java +++ b/spring-binding/src/main/java/org/springframework/binding/convert/service/GenericConversionService.java @@ -386,6 +386,15 @@ public class GenericConversionService implements ConversionService { } } + public Object executeConversion(String converterId, Object source, Class targetClass) throws ConversionException { + if (source != null) { + ConversionExecutor conversionExecutor = getConversionExecutor(converterId, source.getClass(), targetClass); + return conversionExecutor.execute(source); + } else { + return null; + } + } + public Class getClassForAlias(String name) throws IllegalArgumentException { Class clazz = (Class) aliasMap.get(name); if (clazz != null) { diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/PropertyNotFoundException.java b/spring-binding/src/main/java/org/springframework/binding/expression/PropertyNotFoundException.java index 5bc5b6a6..33f7466b 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/PropertyNotFoundException.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/PropertyNotFoundException.java @@ -29,8 +29,7 @@ public class PropertyNotFoundException extends EvaluationException { * @param cause root cause of the failure */ public PropertyNotFoundException(Class contextClass, String property, Throwable cause) { - super(contextClass, property, "Property '" + property + "' not found on context of class [" - + contextClass.getName() + "]", cause); + super(contextClass, property, "Property not found", cause); } } diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/ValueCoercionException.java b/spring-binding/src/main/java/org/springframework/binding/expression/ValueCoercionException.java index 14aba128..c7b595af 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/ValueCoercionException.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/ValueCoercionException.java @@ -48,8 +48,8 @@ public class ValueCoercionException extends EvaluationException { * @param cause root cause of the failure */ public ValueCoercionException(Class contextClass, String property, Object value, Class targetClass, Throwable cause) { - super(contextClass, property, "Value [" + value + "] could not be coerced to type [" + targetClass.getName() - + "]", cause); + super(contextClass, property, + "Value could not be converted to target class; is a suitable type converter registered?", cause); this.value = value; this.targetClass = targetClass; } diff --git a/spring-binding/src/main/java/org/springframework/binding/message/MessageContextErrors.java b/spring-binding/src/main/java/org/springframework/binding/message/MessageContextErrors.java index f83581a7..9363034a 100644 --- a/spring-binding/src/main/java/org/springframework/binding/message/MessageContextErrors.java +++ b/spring-binding/src/main/java/org/springframework/binding/message/MessageContextErrors.java @@ -72,20 +72,21 @@ public class MessageContextErrors extends AbstractErrors { } public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) { - messageContext.addMessage(new MessageBuilder().error().source(field).code(errorCode).args(errorArgs) - .defaultText(defaultMessage).build()); + messageContext.addMessage(new MessageBuilder().error().source(fixedField(field)).code(errorCode) + .args(errorArgs).defaultText(defaultMessage).build()); } public void addAllErrors(Errors errors) { Iterator it = errors.getAllErrors().iterator(); while (it.hasNext()) { ObjectError error = (ObjectError) it.next(); + MessageBuilder builder = new MessageBuilder().error().codes(error.getCodes()).args(error.getArguments()) + .defaultText(error.getDefaultMessage()); if (error instanceof FieldError) { FieldError fieldError = (FieldError) error; - rejectValue(fieldError.getField(), error.getCode(), error.getArguments(), error.getDefaultMessage()); - } else { - reject(error.getCode(), error.getArguments(), error.getDefaultMessage()); + builder.source(fieldError.getField()); } + messageContext.addMessage(builder.build()); } } @@ -103,7 +104,7 @@ public class MessageContextErrors extends AbstractErrors { Message message = messages[i]; errors.add(new ObjectError(objectName, message.getText())); } - return errors; + return Collections.unmodifiableList(errors); } public List getFieldErrors() { @@ -116,11 +117,12 @@ public class MessageContextErrors extends AbstractErrors { Message message = messages[i]; errors.add(new FieldError(objectName, (String) message.getSource(), message.getText())); } - return errors; + return Collections.unmodifiableList(errors); } public Object getFieldValue(String field) { - // requires boundObject, and expressionParser to work + field = fixedField(field); + // requires boundObject and expressionParser to be set to work if (mappingResults != null) { List results = mappingResults.getResults(new PropertyErrorMappingResult(field)); if (!results.isEmpty()) { @@ -131,13 +133,15 @@ public class MessageContextErrors extends AbstractErrors { return parseFieldExpression(field).getValue(boundObject); } + // internal helpers + private Expression parseFieldExpression(String field) { return expressionParser.parseExpression(field, new FluentParserContext().evaluate(boundObject.getClass())); } private static MessageCriteria GLOBAL_ERROR = new MessageCriteria() { public boolean test(Message message) { - if (message.getSource() == null && message.getSeverity().equals(Severity.ERROR)) { + if (message.getSeverity() == Severity.ERROR && message.getSource() == null) { return true; } else { return false; @@ -147,7 +151,7 @@ public class MessageContextErrors extends AbstractErrors { private static MessageCriteria FIELD_ERROR = new MessageCriteria() { public boolean test(Message message) { - if (message.getSource() != null && message.getSeverity().equals(Severity.ERROR)) { + if (message.getSeverity() == Severity.ERROR && message.getSource() instanceof String) { return true; } else { return false; diff --git a/spring-faces/src/test/java/org/springframework/faces/config/FacesFlowBuilderServicesBeanDefinitionParserTests.java b/spring-faces/src/test/java/org/springframework/faces/config/FacesFlowBuilderServicesBeanDefinitionParserTests.java index 056ec868..93f3c250 100644 --- a/spring-faces/src/test/java/org/springframework/faces/config/FacesFlowBuilderServicesBeanDefinitionParserTests.java +++ b/spring-faces/src/test/java/org/springframework/faces/config/FacesFlowBuilderServicesBeanDefinitionParserTests.java @@ -93,6 +93,10 @@ public class FacesFlowBuilderServicesBeanDefinitionParserTests extends TestCase throw new UnsupportedOperationException("Auto-generated method stub"); } + public Object executeConversion(String converterId, Object source, Class targetClass) { + throw new UnsupportedOperationException("Auto-generated method stub"); + } + public ConversionExecutor getConversionExecutor(Class sourceClass, Class targetClass) throws ConversionExecutionException { throw new UnsupportedOperationException("Auto-generated method stub"); diff --git a/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Amenity.java b/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Amenity.java index 9b1c2255..54d381e8 100644 --- a/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Amenity.java +++ b/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Amenity.java @@ -1,5 +1,31 @@ package org.springframework.webflow.samples.booking; -public enum Amenity { - OCEAN_VIEW, LATE_CHECKOUT, MINIBAR; -} +import java.io.Serializable; + +public class Amenity implements Serializable { + + private String name; + + public Amenity(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Amenity)) { + return false; + } + Amenity a = (Amenity) obj; + return name.equals(a.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + +} \ No newline at end of file diff --git a/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/ApplicationConversionService.java b/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/ApplicationConversionService.java index b921bf13..00d188ae 100644 --- a/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/ApplicationConversionService.java +++ b/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/ApplicationConversionService.java @@ -13,6 +13,7 @@ public class ApplicationConversionService extends DefaultConversionService { StringToDate dateConverter = new StringToDate(); dateConverter.setPattern("MM-dd-yyyy"); addConverter("shortDate", dateConverter); + addConverter(new StringToAmenity()); } } \ No newline at end of file diff --git a/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Booking.java b/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Booking.java index ecc1db34..267ccde4 100755 --- a/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Booking.java +++ b/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Booking.java @@ -5,6 +5,7 @@ import java.math.BigDecimal; import java.text.DateFormat; import java.util.Calendar; import java.util.Date; +import java.util.Iterator; import java.util.Set; import javax.persistence.Basic; @@ -186,6 +187,12 @@ public class Booking implements Serializable { } public void setAmenities(Set amenities) { + Iterator it = amenities.iterator(); + while (it.hasNext()) { + Object a = it.next(); + System.out.println(a); + System.out.println(a.getClass()); + } this.amenities = amenities; } diff --git a/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/StringToAmenity.java b/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/StringToAmenity.java new file mode 100644 index 00000000..6c22befc --- /dev/null +++ b/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/StringToAmenity.java @@ -0,0 +1,21 @@ +package org.springframework.webflow.samples.booking; + +import org.springframework.binding.convert.converters.StringToObject; + +public class StringToAmenity extends StringToObject { + + public StringToAmenity() { + super(Amenity.class); + } + + @Override + protected Object toObject(String string, Class tagetClass) throws Exception { + return new Amenity(string); + } + + @Override + protected String toString(Object value) throws Exception { + return ((Amenity) value).getName(); + } + +} diff --git a/spring-webflow-samples/booking-mvc/src/main/webapp/WEB-INF/config/webflow-config.xml b/spring-webflow-samples/booking-mvc/src/main/webapp/WEB-INF/config/webflow-config.xml index 30c37c14..9ff6bd1a 100644 --- a/spring-webflow-samples/booking-mvc/src/main/webapp/WEB-INF/config/webflow-config.xml +++ b/spring-webflow-samples/booking-mvc/src/main/webapp/WEB-INF/config/webflow-config.xml @@ -26,7 +26,8 @@ - + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderContextImpl.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderContextImpl.java index c5cb753b..559e49af 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderContextImpl.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderContextImpl.java @@ -127,7 +127,7 @@ public class FlowBuilderContextImpl implements FlowBuilderContext { this.flowAttributes = CollectionUtils.EMPTY_ATTRIBUTE_MAP; } } - + /** * A little proxy that refreshes the externally configured conversion service reference on each invocation. */ @@ -136,6 +136,10 @@ public class FlowBuilderContextImpl implements FlowBuilderContext { return getFlowBuilderServices().getConversionService().executeConversion(source, targetClass); } + public Object executeConversion(String converterId, Object source, Class targetClass) { + return getFlowBuilderServices().getConversionService().executeConversion(converterId, source, targetClass); + } + public ConversionExecutor getConversionExecutor(Class sourceClass, Class targetClass) throws ConversionExecutionException { return getFlowBuilderServices().getConversionService().getConversionExecutor(sourceClass, targetClass); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/BindingModel.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/BindingModel.java index a42344c8..5c0703e2 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/BindingModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/BindingModel.java @@ -15,12 +15,14 @@ */ package org.springframework.webflow.mvc.view; +import java.beans.PropertyEditor; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import org.springframework.beans.PropertyEditorRegistry; import org.springframework.binding.convert.ConversionExecutor; import org.springframework.binding.convert.ConversionService; import org.springframework.binding.expression.Expression; @@ -35,6 +37,7 @@ import org.springframework.binding.message.MessageCriteria; import org.springframework.binding.message.Severity; import org.springframework.util.Assert; import org.springframework.validation.AbstractErrors; +import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; @@ -51,7 +54,7 @@ import org.springframework.webflow.engine.builder.BinderConfiguration.Binding; * * @author Keith Donald */ -public class BindingModel extends AbstractErrors { +public class BindingModel extends AbstractErrors implements BindingResult { private String objectName; @@ -110,14 +113,23 @@ public class BindingModel extends AbstractErrors { } public List getFieldErrors(String field) { - return toErrors(messageContext.getMessagesByCriteria(new FieldErrorMessage(field))); + field = fixedField(field); + MessageCriteria messageCriteria; + if (field.endsWith("*")) { + String prefix = field.substring(0, field.length() - 1); + messageCriteria = new FieldPrefixErrorMessage(prefix); + } else { + messageCriteria = new FieldErrorMessage(field); + } + return toErrors(messageContext.getMessagesByCriteria(messageCriteria)); } public Class getFieldType(String field) { - return parseFieldExpression(field).getValueType(boundObject); + return parseFieldExpression(fixedField(field)).getValueType(boundObject); } public Object getFieldValue(String field) { + field = fixedField(field); if (mappingResults != null) { List results = mappingResults.getResults(new FieldErrorResult(field)); if (!results.isEmpty()) { @@ -131,7 +143,82 @@ public class BindingModel extends AbstractErrors { // not typically used by mvc views, but implemented to be on the safe side public List getFieldErrors() { - return toErrors(messageContext.getMessagesByCriteria(new FieldErrorMessage())); + return toErrors(messageContext.getMessagesByCriteria(ERRORS_FIELD_SOURCE)); + } + + public String getObjectName() { + return objectName; + } + + // never expected to be called by mvc views + + public void addAllErrors(Errors errors) { + throw new UnsupportedOperationException("Should not be called during view rendering"); + } + + public void reject(String errorCode, Object[] errorArgs, String defaultMessage) { + throw new UnsupportedOperationException("Should not be called during view rendering"); + } + + public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) { + throw new UnsupportedOperationException("Should not be called during view rendering"); + } + + // implementing BindingResult + + public Object getTarget() { + return boundObject; + } + + public Object getRawFieldValue(String field) { + return parseFieldExpression(fixedField(field)).getValue(boundObject); + } + + public PropertyEditor findEditor(String field, Class valueType) { + if (conversionService != null) { + String converterId = null; + if (field != null) { + field = fixedField(field); + if (binderConfiguration != null) { + Binding binding = binderConfiguration.getBinding(field); + if (binding != null) { + converterId = binding.getConverter(); + } + } + if (valueType == null) { + valueType = parseFieldExpression(field).getValueType(boundObject); + } + } + return new ConversionExecutorPropertyEditor(conversionService, valueType, converterId); + } else { + return null; + } + } + + // never expected to be called by mvc views + + public void addError(ObjectError error) { + throw new UnsupportedOperationException("Should not be called during view rendering"); + } + + public Map getModel() { + throw new UnsupportedOperationException("Should not be called during view rendering"); + } + + public PropertyEditorRegistry getPropertyEditorRegistry() { + throw new UnsupportedOperationException("Should not be called during view rendering"); + } + + public String[] getSuppressedFields() { + throw new UnsupportedOperationException("Should not be called during view rendering"); + } + + public void recordSuppressedField(String field) { + throw new UnsupportedOperationException("Should not be called during view rendering"); + } + + public String[] resolveMessageCodes(String errorCode, String field) { + throw new UnsupportedOperationException("Should not be called during view rendering"); } // internal helpers @@ -187,7 +274,7 @@ public class BindingModel extends AbstractErrors { errors.add(new FieldError(objectName, (String) message.getSource(), message.getText())); } } - return errors; + return Collections.unmodifiableList(errors); } private static class FieldErrorResult implements MappingResultsCriteria { @@ -219,39 +306,37 @@ public class BindingModel extends AbstractErrors { } }; + private static final MessageCriteria ERRORS_FIELD_SOURCE = new MessageCriteria() { + public boolean test(Message message) { + return message.getSeverity() == Severity.ERROR && message.getSource() instanceof String; + } + }; + private static class FieldErrorMessage implements MessageCriteria { private String field; - public FieldErrorMessage() { - } - public FieldErrorMessage(String field) { + Assert.hasText(field, "The field name is required"); this.field = field; } public boolean test(Message message) { - if (field != null) { - return field.equals(message.getSource()) && message.getSeverity() == Severity.ERROR; - } else { - return message.getSource() != null && message.getSeverity() == Severity.ERROR; - } + return message.getSeverity() == Severity.ERROR && field.equals(message.getSource()); } } - public String getObjectName() { - return objectName; + private static class FieldPrefixErrorMessage implements MessageCriteria { + private String fieldPrefix; + + public FieldPrefixErrorMessage(String fieldPrefix) { + Assert.hasText(fieldPrefix, "The fieldPrefix is required"); + this.fieldPrefix = fieldPrefix; + } + + public boolean test(Message message) { + return message.getSeverity() == Severity.ERROR && message.getSource() instanceof String + && ((String) message.getSource()).startsWith(fieldPrefix); + } } - public void addAllErrors(Errors errors) { - throw new UnsupportedOperationException("Should not be called during view rendering"); - } - - public void reject(String errorCode, Object[] errorArgs, String defaultMessage) { - throw new UnsupportedOperationException("Should not be called during view rendering"); - } - - public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) { - throw new UnsupportedOperationException("Should not be called during view rendering"); - } - -} +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/ConversionExecutorPropertyEditor.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/ConversionExecutorPropertyEditor.java new file mode 100644 index 00000000..f4f2394b --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/ConversionExecutorPropertyEditor.java @@ -0,0 +1,54 @@ +/* + * Copyright 2004-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.mvc.view; + +import java.beans.PropertyEditorSupport; + +import org.springframework.binding.convert.ConversionService; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +class ConversionExecutorPropertyEditor extends PropertyEditorSupport { + + private ConversionService conversionService; + + private Class fieldType; + + private String converterId; + + public ConversionExecutorPropertyEditor(ConversionService conversionService, Class fieldType, String converterId) { + Assert.notNull(fieldType, "The field type is required"); + this.conversionService = conversionService; + this.fieldType = fieldType; + this.converterId = converterId; + } + + public String getAsText() { + if (StringUtils.hasText(converterId)) { + return (String) conversionService.executeConversion(converterId, getValue(), String.class); + } else { + return (String) conversionService.executeConversion(getValue(), String.class); + } + } + + public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(converterId)) { + setValue(conversionService.executeConversion(converterId, text, fieldType)); + } else { + setValue(conversionService.executeConversion(text, fieldType)); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java index cb1997f3..be1e03c3 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java @@ -30,28 +30,26 @@ import org.springframework.webflow.execution.FlowExecutionOutcome; import org.springframework.webflow.test.MockExternalContext; /** - * Base class for integration tests that verify a flow executes as expected. Flow execution tests captured by subclasses - * should test that a flow responds to all supported transition criteria correctly, transitioning to the correct states - * and producing the expected results on the occurrence of possible external (user) events. + * Base class for tests that verify a flow executes as expected. Flow execution tests authored by subclasses should test + * that a flow responds to all supported transition criteria correctly, transitioning to the correct states and + * producing the expected results on the occurrence of external events. *

- * More specifically, a typical flow execution test case will test: + * A typical flow execution test case will test: *

*

* A flow execution test can effectively automate and validate the orchestration required to drive an end-to-end * business task that spans several steps involving the user to complete. Such tests are a good way to test your system * top-down starting at the web-tier and pushing through all the way to the DB without having to deploy to a servlet or * portlet container. In addition, they can be used to effectively test a flow's execution (the web layer) standalone, - * typically with a mock service layer. Both styles of testing are valuable and supported. + * typically with a mock service layer. * * @author Keith Donald */ @@ -311,14 +309,15 @@ public abstract class AbstractFlowExecutionTests extends TestCase { } /** - * Assert that the entire flow execution has ended; that is, it is no longer active. + * Assert that the flow execution has ended; that is, it is no longer active. */ protected void assertFlowExecutionEnded() { assertTrue("The flow execution is still active but it should have ended", getFlowExecution().hasEnded()); } /** - * Assert that the entire flow execution has ended; that is, it is no longer active. + * Assert that the flow execution has ended with the outcome specified. + * @param outcome the name of the flow execution outcome */ protected void assertFlowExecutionOutcomeEquals(String outcome) { assertNotNull("There has been no flow execution outcome", flowExecutionOutcome); diff --git a/spring-webflow/src/test/java/org/springframework/webflow/TestBean.java b/spring-webflow/src/test/java/org/springframework/webflow/TestBean.java index fe7b3743..875051e5 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/TestBean.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/TestBean.java @@ -78,4 +78,5 @@ public class TestBean implements Serializable { public int hashCode() { return (datum1.hashCode() + datum2 + (executed ? 1 : 0)) * 29; } + } \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParserTests.java b/spring-webflow/src/test/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParserTests.java index 392bb001..33a603d1 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParserTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParserTests.java @@ -76,6 +76,10 @@ public class FlowBuilderServicesBeanDefinitionParserTests extends TestCase { throw new UnsupportedOperationException("Auto-generated method stub"); } + public Object executeConversion(String converterId, Object source, Class targetClass) { + throw new UnsupportedOperationException("Auto-generated method stub"); + } + public ConversionExecutor getConversionExecutor(Class sourceClass, Class targetClass) throws ConversionExecutionException { throw new UnsupportedOperationException("Auto-generated method stub"); diff --git a/spring-webflow/src/test/java/org/springframework/webflow/mvc/view/BindingModelTests.java b/spring-webflow/src/test/java/org/springframework/webflow/mvc/view/BindingModelTests.java new file mode 100644 index 00000000..c9cd8acf --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/mvc/view/BindingModelTests.java @@ -0,0 +1,209 @@ +package org.springframework.webflow.mvc.view; + +import java.beans.PropertyEditor; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.binding.convert.converters.StringToObject; +import org.springframework.binding.convert.service.DefaultConversionService; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.mapping.Mapping; +import org.springframework.binding.mapping.impl.DefaultMappingResults; +import org.springframework.binding.mapping.results.TypeConversionError; +import org.springframework.binding.message.DefaultMessageContext; +import org.springframework.binding.message.MessageBuilder; +import org.springframework.validation.FieldError; +import org.springframework.webflow.TestBean; +import org.springframework.webflow.engine.builder.BinderConfiguration; +import org.springframework.webflow.engine.builder.BinderConfiguration.Binding; +import org.springframework.webflow.expression.DefaultExpressionParserFactory; + +public class BindingModelTests extends TestCase { + + BindingModel model; + DefaultMessageContext messages; + DefaultConversionService conversionService; + TestBean testBean; + ExpressionParser expressionParser; + + public void setUp() { + testBean = new TestBean(); + messages = new DefaultMessageContext(); + conversionService = new DefaultConversionService(); + expressionParser = DefaultExpressionParserFactory.getExpressionParser(); + model = new BindingModel("testBean", testBean, expressionParser, conversionService, messages); + } + + public void testInitialState() { + assertEquals(0, model.getErrorCount()); + assertEquals(0, model.getFieldErrorCount()); + assertEquals(0, model.getFieldErrorCount("datum1")); + assertEquals(0, model.getGlobalErrorCount()); + assertEquals(0, model.getAllErrors().size()); + assertEquals(0, model.getFieldErrors().size()); + assertNull(model.getFieldError("datum1")); + assertEquals(String.class, model.getFieldType("datum1")); + } + + public void testGetValue() { + testBean.datum1 = "test"; + assertEquals("test", model.getFieldValue("datum1")); + } + + public void testGetConvertedValue() { + testBean.datum2 = 3; + assertEquals("3", model.getFieldValue("datum2")); + } + + public void testGetRawValue() { + testBean.datum2 = 3; + assertEquals(new Integer(3), model.getRawFieldValue("datum2")); + } + + public void testGetFieldValueNonStringNoConversionService() { + model = new BindingModel("testBean", testBean, DefaultExpressionParserFactory.getExpressionParser(), null, + messages); + testBean.datum2 = 3; + assertEquals(new Integer(3), model.getFieldValue("datum2")); + } + + public void testGetFieldValueConvertedWithCustomConverter() { + testBean.datum2 = 3; + conversionService.addConverter("customConverter", new StringToObject(Integer.class) { + protected Object toObject(String string, Class targetClass) throws Exception { + return Integer.valueOf(string); + } + + protected String toString(Object object) throws Exception { + return "$" + object; + } + }); + BinderConfiguration binder = new BinderConfiguration(); + binder.addBinding(new Binding("datum2", "customConverter", true)); + model.setBinderConfiguration(binder); + assertEquals("$3", model.getFieldValue("datum2")); + } + + public void testGetFieldValueError() { + Map source = new HashMap(); + source.put("datum2", "bogus"); + List mappingResults = new ArrayList(); + Mapping mapping = new Mapping() { + public Expression getSourceExpression() { + return expressionParser.parseExpression("datum2", null); + } + + public Expression getTargetExpression() { + return expressionParser.parseExpression("datum2", null); + } + + public boolean isRequired() { + return true; + } + }; + mappingResults.add(new TypeConversionError(mapping, "bogus", null)); + DefaultMappingResults results = new DefaultMappingResults(source, testBean, mappingResults); + model.setMappingResults(results); + assertEquals("bogus", model.getFieldValue("datum2")); + // not offically an error until an actual error message is associated with field + assertEquals(0, model.getErrorCount()); + assertEquals(0, model.getFieldErrorCount()); + } + + public void testGetFieldError() { + messages.addMessage(new MessageBuilder().source("datum2").error().defaultText("Error").build()); + assertEquals(1, model.getErrorCount()); + assertEquals(1, model.getFieldErrorCount()); + assertEquals(0, model.getGlobalErrorCount()); + + FieldError error = model.getFieldError("datum2"); + assertEquals(null, error.getCode()); + assertEquals(null, error.getCodes()); + assertEquals(null, error.getArguments()); + assertEquals("Error", error.getDefaultMessage()); + // we dont track this + assertEquals(null, error.getRejectedValue()); + assertTrue(!error.isBindingFailure()); + + FieldError error2 = (FieldError) model.getFieldErrors().get(0); + assertEquals(error, error2); + } + + public void testGetFieldErrorsWildcard() { + messages.addMessage(new MessageBuilder().source("datum2").error().defaultText("Error").build()); + assertEquals(1, model.getFieldErrorCount("da*")); + FieldError error = model.getFieldError("da*"); + assertEquals(null, error.getCode()); + assertEquals(null, error.getCodes()); + assertEquals(null, error.getArguments()); + assertEquals("Error", error.getDefaultMessage()); + } + + public void testFindPropertyEditor() { + PropertyEditor editor = model.findEditor("datum2", Integer.class); + assertNotNull(editor); + editor.setAsText((String) model.getFieldValue("datum2")); + assertEquals("0", editor.getAsText()); + } + + public void testNestedPath() { + model = new BindingModel("nestedPathBean", new NestedPathBean(), expressionParser, conversionService, messages); + model.pushNestedPath("nestedBean"); + assertEquals("test", model.getFieldValue("datum1")); + assertEquals("0", model.getFieldValue("datum2")); + assertEquals(int.class, model.getFieldType("datum2")); + + messages.addMessage(new MessageBuilder().source("nestedBean.datum2").error().defaultText("Error").build()); + assertNotNull(model.getFieldErrors("datum2").get(0)); + model.popNestedPath(); + assertEquals("", model.getFieldValue("datum1")); + } + + public static class NestedPathBean { + private String datum1 = ""; + + private NestedBean nestedBean = new NestedBean(); + + public String getDatum1() { + return datum1; + } + + public void setDatum1(String datum1) { + this.datum1 = datum1; + } + + public NestedBean getNestedBean() { + return nestedBean; + } + + public void setNestedBean(NestedBean nestedBean) { + this.nestedBean = nestedBean; + } + + public static class NestedBean { + private String datum1 = "test"; + private int datum2; + + public int getDatum2() { + return datum2; + } + + public void setDatum2(int datum2) { + this.datum2 = datum2; + } + + public String getDatum1() { + return datum1; + } + + public void setDatum1(String datum1) { + this.datum1 = datum1; + } + } + } +}