diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/GenericConversionService.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/GenericConversionService.java index e2d42b82..5f0aa7c1 100644 --- a/spring-binding/src/main/java/org/springframework/binding/convert/support/GenericConversionService.java +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/GenericConversionService.java @@ -117,6 +117,7 @@ public class GenericConversionService implements ConversionService { * Add an alias for given target type. */ public void addAlias(String alias, Class targetType) { + Assert.isTrue(!targetType.isPrimitive(), "Primitive types cannot be registered"); aliasMap.put(alias, targetType); } @@ -137,6 +138,8 @@ public class GenericConversionService implements ConversionService { if (targetClass.isAssignableFrom(sourceClass)) { return new ConversionExecutorImpl(sourceClass, targetClass, new NoOpConverter(sourceClass, targetClass)); } + sourceClass = convertToWrapperClassIfNecessary(sourceClass); + targetClass = convertToWrapperClassIfNecessary(targetClass); Map sourceTargetConverters = findConvertersForSource(sourceClass); Converter converter = findTargetConverter(sourceTargetConverters, targetClass); if (converter != null) { @@ -220,6 +223,25 @@ public class GenericConversionService implements ConversionService { } } + // subclassing support + + /** + * Returns an indexed map of converters. Each entry key is a source class that can be converted from, and each entry + * value is a map of target classes that can be convertered to, ultimately mapping to a specific converter that can + * perform the source->target conversion. + */ + protected Map getSourceClassConverters() { + return sourceClassConverters; + } + + /** + * Returns a map of known aliases. Each entry key is a String alias and the associated value is either a target + * class or a converter. + */ + protected Map getAliasMap() { + return aliasMap; + } + // internal helpers private Map findConvertersForSource(Class sourceClass) { @@ -264,22 +286,29 @@ public class GenericConversionService implements ConversionService { return null; } - // subclassing support - - /** - * Returns an indexed map of converters. Each entry key is a source class that can be converted from, and each entry - * value is a map of target classes that can be convertered to, ultimately mapping to a specific converter that can - * perform the source->target conversion. - */ - protected Map getSourceClassConverters() { - return sourceClassConverters; - } - - /** - * Returns a map of known aliases. Each entry key is a String alias and the associated value is either a target - * class or a converter. - */ - protected Map getAliasMap() { - return aliasMap; + private Class convertToWrapperClassIfNecessary(Class targetType) { + if (targetType.isPrimitive()) { + if (targetType.equals(int.class)) { + return Integer.class; + } else if (targetType.equals(short.class)) { + return Short.class; + } else if (targetType.equals(long.class)) { + return Long.class; + } else if (targetType.equals(float.class)) { + return Float.class; + } else if (targetType.equals(double.class)) { + return Double.class; + } else if (targetType.equals(byte.class)) { + return Byte.class; + } else if (targetType.equals(boolean.class)) { + return Boolean.class; + } else if (targetType.equals(char.class)) { + return Character.class; + } else { + throw new IllegalStateException("Should never happen - primitive type is not a primitive?"); + } + } else { + return targetType; + } } } \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToNumber.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToNumber.java index 9f67a642..cea3982a 100644 --- a/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToNumber.java +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToNumber.java @@ -50,7 +50,7 @@ public class TextToNumber extends AbstractFormattingConverter { } public Class[] getTargetClasses() { - return new Class[] { Integer.class, Short.class, Byte.class, Long.class, Float.class, Double.class, + return new Class[] { Integer.class, Short.class, Long.class, Float.class, Double.class, Byte.class, BigInteger.class, BigDecimal.class }; } diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/Expression.java b/spring-binding/src/main/java/org/springframework/binding/expression/Expression.java index bf6fcb6d..c6394985 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/Expression.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/Expression.java @@ -33,9 +33,24 @@ public interface Expression { /** * Set this expression in the provided context to the value provided. - * @param context the context to apply this value to + * @param context the context to set this value to * @param value the new value to be set * @throws EvaluationException an exception occurred during evaluation */ public void setValue(Object context, Object value) throws EvaluationException; + + /** + * Returns the most general type that can be passed to the {@link #setValue(Object, Object)} method for the given + * context. + * @param context the context of expression evaluation + * @return the most general type of value that can be set + */ + public Class getValueType(Object context); + + /** + * Returns the original string used to create this expression, unmodified. + * @return the original expression string + */ + public String getExpressionString(); + } \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/el/DefaultELResolver.java b/spring-binding/src/main/java/org/springframework/binding/expression/el/DefaultELResolver.java index 8fb98122..f48f6524 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/el/DefaultELResolver.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/el/DefaultELResolver.java @@ -41,7 +41,11 @@ public class DefaultELResolver extends CompositeELResolver { } public Class getType(ELContext context, Object base, Object property) { - return super.getType(context, base, property); + if (base == null) { + return super.getType(context, target, property); + } else { + return super.getType(context, base, property); + } } public Object getValue(ELContext context, Object base, Object property) { diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpression.java index fca7eb59..ac3d85e5 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpression.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpression.java @@ -21,16 +21,21 @@ public class ELExpression implements Expression { private ValueExpression valueExpression; + private boolean template; + /** * Creates a new el expression * @param factory the el context factory for creating the EL context that will be used during expression evaluation * @param valueExpression the value expression to evaluate + * @param template whether or not this expression is a template expression; if not it was parsed as an implict eval + * expression (without delimiters) */ - public ELExpression(ELContextFactory factory, ValueExpression valueExpression) { + public ELExpression(ELContextFactory factory, ValueExpression valueExpression, boolean template) { Assert.notNull(factory, "The ELContextFactory is required to evaluate EL expressions"); Assert.notNull(valueExpression, "The EL value expression is required for evaluation"); this.elContextFactory = factory; this.valueExpression = valueExpression; + this.template = template; } public Object getValue(Object context) throws EvaluationException { @@ -58,6 +63,24 @@ public class ELExpression implements Expression { } } + public Class getValueType(Object context) { + ELContext ctx = elContextFactory.getELContext(context); + try { + return valueExpression.getType(ctx); + } catch (ELException ex) { + throw new EvaluationException(new EvaluationAttempt(this, context), ex); + } + } + + public String getExpressionString() { + if (template) { + return valueExpression.getExpressionString(); + } else { + String rawExpressionString = valueExpression.getExpressionString(); + return rawExpressionString.substring("#{".length(), rawExpressionString.length() - 1); + } + } + public int hashCode() { return valueExpression.hashCode(); } diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpressionParser.java b/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpressionParser.java index 7abe32ab..72f50674 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpressionParser.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpressionParser.java @@ -56,20 +56,21 @@ public class ELExpressionParser implements ExpressionParser { context = NullParserContext.INSTANCE; } if (context.isTemplate()) { - return parseTemplate(expressionString, context); + return parseExpressionInternal(expressionString, context, true); } else { assertNotDelimited(expressionString); assertHasText(expressionString); - return parseTemplate("#{" + expressionString + "}", context); + return parseExpressionInternal("#{" + expressionString + "}", context, false); } } - private Expression parseTemplate(String expressionString, ParserContext context) throws ParserException { + private Expression parseExpressionInternal(String expressionString, ParserContext context, boolean template) + throws ParserException { Assert.notNull(expressionString, "The expression string to parse is required"); try { ValueExpression expression = parseValueExpression(expressionString, context); ELContextFactory contextFactory = getContextFactory(context.getEvaluationContextType(), expressionString); - return new ELExpression(contextFactory, expression); + return new ELExpression(contextFactory, expression, template); } catch (ELException e) { throw new ParserException(expressionString, e); } diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/ognl/OgnlExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/ognl/OgnlExpression.java index 96ea272a..46771f46 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/ognl/OgnlExpression.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/ognl/OgnlExpression.java @@ -52,13 +52,18 @@ class OgnlExpression implements Expression { private Class expectedResultType; /** - * Creates a new OGNL expression. - * @param expression the parsed expression + * The original expression string. */ - public OgnlExpression(Object expression, Map variableExpressions, Class expectedResultType) { + private String expressionString; + + /** + * Creates a new OGNL expression. + */ + public OgnlExpression(Object expression, Map variableExpressions, Class expectedResultType, String expressionString) { this.expression = expression; this.variableExpressions = variableExpressions; this.expectedResultType = expectedResultType; + this.expressionString = expressionString; } public int hashCode() { @@ -98,6 +103,23 @@ class OgnlExpression implements Expression { } } + public Class getValueType(Object context) { + try { + if (Ognl.isSimpleProperty(expression)) { + // TODO + throw new UnsupportedOperationException("Not yet implemented - in progress"); + } else { + return null; + } + } catch (OgnlException e) { + throw new EvaluationException(new EvaluationAttempt(this, context), e); + } + } + + public String getExpressionString() { + return expressionString; + } + private Map getVariables(Object context) { if (variableExpressions == null) { return Collections.EMPTY_MAP; diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/ognl/OgnlExpressionParser.java b/spring-binding/src/main/java/org/springframework/binding/expression/ognl/OgnlExpressionParser.java index ca732cd5..d887d9df 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/ognl/OgnlExpressionParser.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/ognl/OgnlExpressionParser.java @@ -244,7 +244,7 @@ public class OgnlExpressionParser implements ExpressionParser { } try { return new OgnlExpression(Ognl.parseExpression(expressionString), parseVariableExpressions(context - .getExpressionVariables()), context.getExpectedEvaluationResultType()); + .getExpressionVariables()), context.getExpectedEvaluationResultType(), expressionString); } catch (OgnlException e) { throw new ParserException(expressionString, e); } diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractGetValueExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractGetValueExpression.java index 72c31a8f..f0c22f81 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractGetValueExpression.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractGetValueExpression.java @@ -19,4 +19,12 @@ public abstract class AbstractGetValueExpression implements Expression { throw new UnsupportedOperationException("Setting this expression's value is not supported"); } + public Class getValueType(Object context) { + return null; + } + + public String getExpressionString() { + return null; + } + } diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/CollectionAddingExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/CollectionAddingExpression.java index 2e49093d..bdd2cf17 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/support/CollectionAddingExpression.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/CollectionAddingExpression.java @@ -60,6 +60,14 @@ public class CollectionAddingExpression implements Expression { } } + public Class getValueType(Object context) { + return Object.class; + } + + public String getExpressionString() { + return null; + } + public String toString() { return new ToStringCreator(this).append("collectionExpression", collectionExpression).toString(); } diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/CompositeStringExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/CompositeStringExpression.java index efb5b728..b72f4d07 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/support/CompositeStringExpression.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/CompositeStringExpression.java @@ -54,6 +54,14 @@ public class CompositeStringExpression implements Expression { "Cannot set a composite string expression value")); } + public Class getValueType(Object context) { + return String.class; + } + + public String getExpressionString() { + return null; + } + public String toString() { return new ToStringCreator(this).append("expressions", expressions).toString(); } diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/LiteralExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/LiteralExpression.java index 9809d234..6f2245f7 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/support/LiteralExpression.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/LiteralExpression.java @@ -43,6 +43,14 @@ public class LiteralExpression implements Expression { + "If so, should the expression string be enclosed in eval delimiters?")); } + public Class getValueType(Object context) { + return String.class; + } + + public String getExpressionString() { + return null; + } + public String toString() { return "literal('" + literal + "')"; } diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/StaticExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/StaticExpression.java index c7496cd5..7cbd8073 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/support/StaticExpression.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/StaticExpression.java @@ -55,14 +55,22 @@ public final class StaticExpression implements Expression { return ObjectUtils.nullSafeEquals(value, other.value); } - public Object getValue(Object target) throws EvaluationException { + public Object getValue(Object context) throws EvaluationException { return value; } - public void setValue(Object target, Object value) throws EvaluationException { + public void setValue(Object context, Object value) throws EvaluationException { this.value = value; } + public Class getValueType(Object context) { + return Object.class; + } + + public String getExpressionString() { + return null; + } + public String toString() { return String.valueOf(value); } diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/AbstractFormatter.java b/spring-binding/src/main/java/org/springframework/binding/format/support/AbstractFormatter.java index cc0294e1..62cab78b 100644 --- a/spring-binding/src/main/java/org/springframework/binding/format/support/AbstractFormatter.java +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/AbstractFormatter.java @@ -19,7 +19,6 @@ import java.text.ParseException; import org.springframework.binding.format.Formatter; import org.springframework.binding.format.InvalidFormatException; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -29,38 +28,29 @@ import org.springframework.util.StringUtils; */ public abstract class AbstractFormatter implements Formatter { - /** - * Does this formatter allow empty values? - */ - private boolean allowEmpty = true; - /** * Constructs a formatter. */ protected AbstractFormatter() { } - /** - * Constructs a formatter. - * @param allowEmpty allow formatting of empty (null or blank) values? - */ - protected AbstractFormatter(boolean allowEmpty) { - this.allowEmpty = allowEmpty; - } - - /** - * Allow formatting of empty (null or blank) values? - */ - public boolean isAllowEmpty() { - return allowEmpty; - } - public final String formatValue(Object value) { - if (allowEmpty && isEmpty(value)) { + if (isEmpty(value)) { return getEmptyFormattedValue(); + } else { + return doFormatValue(value); + } + } + + public final Object parseValue(String formattedString, Class targetClass) throws InvalidFormatException { + try { + if (isEmpty(formattedString)) { + return getEmptyValue(); + } + return doParseValue(formattedString, targetClass); + } catch (ParseException ex) { + throw new InvalidFormatException(formattedString, getExpectedFormat(targetClass), ex); } - Assert.isTrue(!isEmpty(value), "Object to format cannot be empty"); - return doFormatValue(value); } /** @@ -77,22 +67,12 @@ public abstract class AbstractFormatter implements Formatter { return ""; } - public final Object parseValue(String formattedString, Class targetClass) throws InvalidFormatException { - try { - if (allowEmpty && isEmpty(formattedString)) { - return getEmptyValue(); - } - return doParseValue(formattedString, targetClass); - } catch (ParseException ex) { - throw new InvalidFormatException(formattedString, getExpectedFormat(targetClass), ex); - } - } - /** * Template method subclasses should override to encapsulate parsing logic. * @param formattedString the formatted string to parse + * @param targetClass the target class to convert the formatted value to * @return the parsed value - * @throws InvalidFormatException an exception occured parsing + * @throws InvalidFormatException an exception occurred parsing * @throws ParseException when parse exceptions occur */ protected abstract Object doParseValue(String formattedString, Class targetClass) throws InvalidFormatException, diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/DateFormatter.java b/spring-binding/src/main/java/org/springframework/binding/format/support/DateFormatter.java index 7c938a38..646e16dd 100644 --- a/spring-binding/src/main/java/org/springframework/binding/format/support/DateFormatter.java +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/DateFormatter.java @@ -38,16 +38,6 @@ public class DateFormatter extends AbstractFormatter { this.dateFormat = dateFormat; } - /** - * Constructs a date formatter that will delegate to the specified date format. - * @param dateFormat the date format to use - * @param allowEmpty should this formatter allow empty input arguments? - */ - public DateFormatter(DateFormat dateFormat, boolean allowEmpty) { - super(allowEmpty); - this.dateFormat = dateFormat; - } - // convert from date to string protected String doFormatValue(Object date) { return dateFormat.format((Date) date); diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/LabeledEnumFormatter.java b/spring-binding/src/main/java/org/springframework/binding/format/support/LabeledEnumFormatter.java index c148226c..8df72740 100644 --- a/spring-binding/src/main/java/org/springframework/binding/format/support/LabeledEnumFormatter.java +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/LabeledEnumFormatter.java @@ -36,14 +36,6 @@ public class LabeledEnumFormatter extends AbstractFormatter { public LabeledEnumFormatter() { } - /** - * Create a new LabeledEnum formatter. - * @param allowEmpty should this formatter allow empty input arguments? - */ - public LabeledEnumFormatter(boolean allowEmpty) { - super(allowEmpty); - } - /** * Set the LabeledEnumResolver used. Defaults to {@link StaticLabeledEnumResolver}. */ @@ -58,13 +50,7 @@ public class LabeledEnumFormatter extends AbstractFormatter { } protected Object doParseValue(String formattedString, Class targetClass) throws IllegalArgumentException { - LabeledEnum labeledEnum = labeledEnumResolver.getLabeledEnumByLabel(targetClass, formattedString); - if (!isAllowEmpty()) { - Assert.notNull(labeledEnum, "The label '" + formattedString - + "' did not map to a valid enum instance for type " + targetClass); - Assert.isInstanceOf(targetClass, labeledEnum); - } - return labeledEnum; + return labeledEnumResolver.getLabeledEnumByLabel(targetClass, formattedString); } /** diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/NumberFormatter.java b/spring-binding/src/main/java/org/springframework/binding/format/support/NumberFormatter.java index d1d78565..2666ae62 100644 --- a/spring-binding/src/main/java/org/springframework/binding/format/support/NumberFormatter.java +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/NumberFormatter.java @@ -31,13 +31,6 @@ public class NumberFormatter extends AbstractFormatter { private NumberFormat numberFormat; - /** - * Default constructor. The formatter will use "toString" when formatting a value and "valueOf" when parsing a - * value. - */ - public NumberFormatter() { - } - /** * Create a new number formatter. * @param numberFormat the number format to use @@ -46,16 +39,6 @@ public class NumberFormatter extends AbstractFormatter { this.numberFormat = numberFormat; } - /** - * Create a new number formatter. - * @param numberFormat the number format to use - * @param allowEmpty should this formatter allow empty input arguments? - */ - public NumberFormatter(NumberFormat numberFormat, boolean allowEmpty) { - super(allowEmpty); - this.numberFormat = numberFormat; - } - protected String doFormatValue(Object number) { if (this.numberFormat != null) { // use NumberFormat for rendering value @@ -67,12 +50,11 @@ public class NumberFormatter extends AbstractFormatter { } protected Object doParseValue(String text, Class targetClass) throws IllegalArgumentException { - // use given NumberFormat for parsing text if (this.numberFormat != null) { + // use given NumberFormat for parsing text return NumberUtils.parseNumber(text, targetClass, this.numberFormat); - } - // use default valueOf methods for parsing text - else { + } else { + // use default valueOf methods for parsing text return NumberUtils.parseNumber(text, targetClass); } } diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/SimpleFormatterFactory.java b/spring-binding/src/main/java/org/springframework/binding/format/support/SimpleFormatterFactory.java index ca7ce58a..ae46ae8b 100644 --- a/spring-binding/src/main/java/org/springframework/binding/format/support/SimpleFormatterFactory.java +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/SimpleFormatterFactory.java @@ -42,7 +42,11 @@ public class SimpleFormatterFactory extends AbstractFormatterFactory { } public Formatter getNumberFormatter(Class numberClass) { - return new NumberFormatter(NumberFormat.getNumberInstance(getLocale())); + if (numberClass.equals(Integer.class) || numberClass.equals(int.class)) { + return new NumberFormatter(NumberFormat.getIntegerInstance(getLocale())); + } else { + return new NumberFormatter(NumberFormat.getNumberInstance(getLocale())); + } } public Formatter getCurrencyFormatter() { diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/AttributeMapper.java b/spring-binding/src/main/java/org/springframework/binding/mapping/AttributeMapper.java index 89af6730..adeef839 100644 --- a/spring-binding/src/main/java/org/springframework/binding/mapping/AttributeMapper.java +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/AttributeMapper.java @@ -30,6 +30,7 @@ public interface AttributeMapper { * @param source the source * @param target the target * @param context the mapping context + * @throws AttributeMappingException if errors occurred during the mapping process */ - public void map(Object source, Object target, MappingContext context); + public void map(Object source, Object target, MappingContext context) throws AttributeMappingException; } \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/AttributeMappingException.java b/spring-binding/src/main/java/org/springframework/binding/mapping/AttributeMappingException.java new file mode 100644 index 00000000..f8d17af7 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/AttributeMappingException.java @@ -0,0 +1,5 @@ +package org.springframework.binding.mapping; + +public class AttributeMappingException extends RuntimeException { + +} diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/DefaultAttributeMapper.java b/spring-binding/src/main/java/org/springframework/binding/mapping/DefaultAttributeMapper.java index 5de07f65..c0049d1e 100644 --- a/spring-binding/src/main/java/org/springframework/binding/mapping/DefaultAttributeMapper.java +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/DefaultAttributeMapper.java @@ -38,10 +38,10 @@ public class DefaultAttributeMapper implements AttributeMapper { /** * Add a mapping to this mapper. - * @param mapping the mapping to add (as an AttributeMapper) + * @param mapping the mapping to add * @return this, to support convenient call chaining */ - public DefaultAttributeMapper addMapping(AttributeMapper mapping) { + public DefaultAttributeMapper addMapping(Mapping mapping) { mappings.add(mapping); return this; } @@ -50,7 +50,7 @@ public class DefaultAttributeMapper implements AttributeMapper { * Add a set of mappings. * @param mappings the mappings */ - public void addMappings(AttributeMapper[] mappings) { + public void addMappings(Mapping[] mappings) { if (mappings == null) { return; } @@ -61,18 +61,25 @@ public class DefaultAttributeMapper implements AttributeMapper { * Returns this mapper's list of mappings. * @return the list of mappings */ - public AttributeMapper[] getMappings() { - return (AttributeMapper[]) mappings.toArray(new AttributeMapper[mappings.size()]); + public Mapping[] getMappings() { + return (Mapping[]) mappings.toArray(new Mapping[mappings.size()]); } - public void map(Object source, Object target, MappingContext context) { + public void map(Object source, Object target, MappingContext context) throws AttributeMappingException { + boolean mappingFailure = false; if (mappings != null) { Iterator it = mappings.iterator(); while (it.hasNext()) { - AttributeMapper mapping = (AttributeMapper) it.next(); - mapping.map(source, target, context); + Mapping mapping = (Mapping) it.next(); + boolean result = mapping.map(source, target, context); + if (!result && !mappingFailure) { + mappingFailure = true; + } } } + if (mappingFailure) { + throw new AttributeMappingException(); + } } public String toString() { diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/Mapping.java b/spring-binding/src/main/java/org/springframework/binding/mapping/Mapping.java index f91edadb..11cb66e6 100644 --- a/spring-binding/src/main/java/org/springframework/binding/mapping/Mapping.java +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/Mapping.java @@ -17,10 +17,14 @@ package org.springframework.binding.mapping; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.binding.convert.ConversionException; import org.springframework.binding.convert.ConversionExecutor; import org.springframework.binding.expression.Expression; +import org.springframework.binding.message.MessageBuilder; +import org.springframework.binding.message.MessageResolver; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * A single mapping definition, encapsulating the information necessary to map the result of evaluating an expression on @@ -28,7 +32,7 @@ import org.springframework.util.Assert; * * @author Keith Donald */ -public class Mapping implements AttributeMapper { +public class Mapping { private static final Log logger = LogFactory.getLog(Mapping.class); @@ -85,29 +89,54 @@ public class Mapping implements AttributeMapper { * @param source The source data structure * @param target The target data structure */ - public void map(Object source, Object target, MappingContext context) { - // get source value + public boolean map(Object source, Object target, MappingContext context) { + Assert.notNull(source, "The source to map from is required"); + Assert.notNull(target, "The target to map to is required"); + Assert.notNull(context, "The mapping context is required"); Object sourceValue = sourceExpression.getValue(source); - if (sourceValue == null) { - if (required) { - throw new RequiredMappingException("This mapping is required; evaluation of expression '" - + sourceExpression + "' against source of type [" + source.getClass() - + "] must return a non-null value"); - } else { - // source expression returned no value, simply abort mapping - return; + if (required && sourceValue == null || isEmptyString(sourceValue)) { + String defaultText = "'" + targetExpression.getExpressionString() + "' is required"; + MessageResolver message = new MessageBuilder().error().source(targetExpression.getExpressionString()) + .codes(createMessageCodes("required", target, targetExpression)).defaultText(defaultText).build(); + context.getMessageContext().addMessage(message); + return false; + } + Object targetValue; + if (sourceValue != null && typeConverter != null) { + try { + targetValue = typeConverter.execute(sourceValue); + } catch (ConversionException e) { + String defaultText = "The '" + targetExpression.getExpressionString() + "' value is the wrong type"; + MessageResolver message = new MessageBuilder().error().source(targetExpression.getExpressionString()) + .codes(createMessageCodes("typeMismatch", target, targetExpression)).defaultText(defaultText) + .build(); + context.getMessageContext().addMessage(message); + return false; } + } else { + targetValue = sourceValue; } - Object targetValue = sourceValue; - if (typeConverter != null) { - targetValue = typeConverter.execute(sourceValue); - } - // set target value if (logger.isDebugEnabled()) { - logger.debug("Mapping '" + sourceExpression + "' value [" + sourceValue + "] to target property '" - + targetExpression + "'; setting property value to [" + targetValue + "]"); + logger.debug("Mapping '" + sourceExpression + "' value [" + sourceValue + "] to target '" + + targetExpression + "'; setting target value to [" + targetValue + "]"); } targetExpression.setValue(target, targetValue); + return true; + } + + private boolean isEmptyString(Object sourceValue) { + if (sourceValue instanceof CharSequence) { + return ((CharSequence) sourceValue).length() == 0; + } else { + return false; + } + } + + private String[] createMessageCodes(String errorCode, Object target, Expression targetExpression) { + String[] codes = new String[2]; + codes[0] = ClassUtils.getShortName(target.getClass()) + "." + targetExpression.getExpressionString(); + codes[1] = errorCode; + return codes; } public boolean equals(Object o) { diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/MappingContext.java b/spring-binding/src/main/java/org/springframework/binding/mapping/MappingContext.java index 73aa66c9..d30d6daf 100644 --- a/spring-binding/src/main/java/org/springframework/binding/mapping/MappingContext.java +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/MappingContext.java @@ -15,6 +15,8 @@ */ package org.springframework.binding.mapping; +import org.springframework.binding.message.MessageContext; + /** * A context object with two main responsibities: *
    @@ -27,4 +29,10 @@ package org.springframework.binding.mapping; */ public interface MappingContext { + /** + * Returns the message context to use to record errors during the mapping process. + * @return the message context + */ + public MessageContext getMessageContext(); + } diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/MappingContextImpl.java b/spring-binding/src/main/java/org/springframework/binding/mapping/MappingContextImpl.java new file mode 100644 index 00000000..f53d1063 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/MappingContextImpl.java @@ -0,0 +1,17 @@ +package org.springframework.binding.mapping; + +import org.springframework.binding.message.MessageContext; + +public class MappingContextImpl implements MappingContext { + + private MessageContext messageContext; + + public MappingContextImpl(MessageContext messageContext) { + this.messageContext = messageContext; + } + + public MessageContext getMessageContext() { + return messageContext; + } + +} diff --git a/spring-binding/src/test/java/org/springframework/binding/mapping/MappingTests.java b/spring-binding/src/test/java/org/springframework/binding/mapping/MappingTests.java new file mode 100644 index 00000000..efbb9a10 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/mapping/MappingTests.java @@ -0,0 +1,28 @@ +/* + * 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.binding.mapping; + +import junit.framework.TestCase; + +/** + * Unit tests for the {@link org.springframework.binding.mapping.RequiredMapping}. + */ +public class MappingTests extends TestCase { + + public void testMapping() { + + } +} diff --git a/spring-binding/src/test/java/org/springframework/binding/mapping/RequiredMappingTests.java b/spring-binding/src/test/java/org/springframework/binding/mapping/RequiredMappingTests.java deleted file mode 100644 index 5d3cb573..00000000 --- a/spring-binding/src/test/java/org/springframework/binding/mapping/RequiredMappingTests.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.binding.mapping; - -import java.util.HashMap; - -import junit.framework.TestCase; - -import org.springframework.binding.expression.ognl.OgnlExpressionParser; - -/** - * Unit tests for the {@link org.springframework.binding.mapping.RequiredMapping}. - */ -public class RequiredMappingTests extends TestCase { - - public void testRequired() { - MappingBuilder builder = new MappingBuilder(new OgnlExpressionParser()); - Mapping mapping = builder.source("foo").target("bar").required().value(); - HashMap source = new HashMap(); - source.put("foo", "baz"); - HashMap target = new HashMap(); - mapping.map(source, target, null); - assertEquals("baz", target.get("bar")); - } - - public void testRequiredExceptionOnNull() { - MappingBuilder builder = new MappingBuilder(new OgnlExpressionParser()); - Mapping mapping = builder.source("foo").target("bar").required().value(); - HashMap source = new HashMap(); - source.put("foo", null); - HashMap target = new HashMap(); - try { - mapping.map(source, target, null); - } catch (RequiredMappingException e) { - } - } - - public void testRequiredExceptionOnNoKey() { - MappingBuilder builder = new MappingBuilder(new OgnlExpressionParser()); - Mapping mapping = builder.source("foo").target("bar").required().value(); - HashMap source = new HashMap(); - HashMap target = new HashMap(); - try { - mapping.map(source, target, null); - } catch (RequiredMappingException e) { - } - } - -} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/AttributeMapperAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/AttributeMapperAction.java index b70c5df8..fe60e204 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/action/AttributeMapperAction.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/AttributeMapperAction.java @@ -17,6 +17,7 @@ package org.springframework.webflow.action; import org.springframework.binding.mapping.AttributeMapper; import org.springframework.binding.mapping.MappingContext; +import org.springframework.binding.mapping.MappingContextImpl; import org.springframework.util.Assert; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; @@ -63,6 +64,6 @@ public class AttributeMapperAction extends AbstractAction { * returns null. Subclasses can override this if necessary. */ protected MappingContext getMappingContext(RequestContext context) { - return null; + return new MappingContextImpl(context.getMessageContext()); } } \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/BindAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/BindAction.java new file mode 100644 index 00000000..3bcc7ca8 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/BindAction.java @@ -0,0 +1,58 @@ +package org.springframework.webflow.action; + +import java.util.Iterator; + +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.convert.support.RuntimeBindingConversionExecutor; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.support.ParserContextImpl; +import org.springframework.binding.mapping.AttributeMappingException; +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.Mapping; +import org.springframework.binding.mapping.MappingContextImpl; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +public class BindAction extends AbstractAction { + + private Expression target; + + private ExpressionParser expressionParser; + + private ConversionService conversionService; + + public BindAction(Expression target, ExpressionParser expressionParser, ConversionService conversionService) { + this.target = target; + this.expressionParser = expressionParser; + this.conversionService = conversionService; + } + + protected Event doExecute(final RequestContext context) throws Exception { + Object target = this.target.getValue(context); + if (target == null) { + throw new IllegalStateException( + "The bind target cannot be null - check your expression. Bind target expression = " + target); + } + DefaultAttributeMapper mapper = new DefaultAttributeMapper(); + AttributeMap eventAttributes = context.getLastEvent().getAttributes(); + for (Iterator it = eventAttributes.asMap().keySet().iterator(); it.hasNext();) { + String name = (String) it.next(); + Expression sourceAttribute = expressionParser.parseExpression(name, new ParserContextImpl() + .eval(AttributeMap.class)); + Expression targetAttribute = expressionParser.parseExpression(name, new ParserContextImpl().eval(target + .getClass())); + Class targetType = targetAttribute.getValueType(target); + mapper.addMapping(new Mapping(sourceAttribute, targetAttribute, new RuntimeBindingConversionExecutor( + targetType, conversionService), false)); + } + try { + mapper.map(context.getLastEvent().getAttributes(), target, new MappingContextImpl(context + .getMessageContext())); + return success(); + } catch (AttributeMappingException e) { + return error(); + } + } +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/FlowException.java b/spring-webflow/src/main/java/org/springframework/webflow/core/FlowException.java index f5141198..cf8af581 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/core/FlowException.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/FlowException.java @@ -15,8 +15,6 @@ */ package org.springframework.webflow.core; -import org.springframework.core.NestedRuntimeException; - /** * Root class for exceptions thrown by the Spring Web Flow system. All other exceptions within the system should be * assignable to this class. @@ -24,7 +22,7 @@ import org.springframework.core.NestedRuntimeException; * @author Keith Donald * @author Erwin Vervaet */ -public abstract class FlowException extends NestedRuntimeException { +public abstract class FlowException extends RuntimeException { /** * Creates a new flow exception. diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/EndState.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/EndState.java index 6d3ce227..434d9c87 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/EndState.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/EndState.java @@ -16,9 +16,8 @@ package org.springframework.webflow.engine; import org.springframework.binding.mapping.AttributeMapper; -import org.springframework.binding.mapping.MappingContext; +import org.springframework.binding.mapping.MappingContextImpl; import org.springframework.core.style.ToStringCreator; -import org.springframework.util.Assert; import org.springframework.webflow.core.collection.LocalAttributeMap; import org.springframework.webflow.execution.Action; import org.springframework.webflow.execution.Event; @@ -49,12 +48,12 @@ public class EndState extends State { /** * The renderer that will render the final response when a flow execution terminates. */ - private Action finalResponseAction = new NullFinalResponseAction(); + private Action finalResponseAction; /** * The attribute mapper for mapping output attributes exposed by this end state when it is entered. */ - private AttributeMapper outputMapper = new NoOutputMapper(); + private AttributeMapper outputMapper; /** * Create a new end state with no associated view. @@ -73,7 +72,6 @@ public class EndState extends State { * Sets the renderer that will render the final flow execution response. */ public void setFinalResponseAction(Action finalResponseAction) { - Assert.notNull(finalResponseAction, "The final response action is required"); this.finalResponseAction = finalResponseAction; } @@ -81,7 +79,6 @@ public class EndState extends State { * Sets the attribute mapper to use for mapping output attributes exposed by this end state when it is entered. */ public void setOutputMapper(AttributeMapper outputMapper) { - Assert.notNull(outputMapper, "The flow session output mapper is required"); this.outputMapper = outputMapper; } @@ -99,7 +96,9 @@ public class EndState extends State { FlowSession activeSession = context.getFlowExecutionContext().getActiveSession(); if (activeSession.isRoot()) { // entire flow execution is ending; issue the final response - ActionExecutor.execute(finalResponseAction, context); + if (finalResponseAction != null) { + ActionExecutor.execute(finalResponseAction, context); + } context.endActiveFlowSession(createSessionOutput(context)); } else { // there is a parent flow that will resume (this flow is a subflow) @@ -115,7 +114,9 @@ public class EndState extends State { */ protected LocalAttributeMap createSessionOutput(RequestContext context) { LocalAttributeMap outputMap = new LocalAttributeMap(); - outputMapper.map(context, outputMap, null); + if (outputMapper != null) { + outputMapper.map(context, outputMap, new MappingContextImpl(context.getMessageContext())); + } return outputMap; } @@ -123,29 +124,4 @@ public class EndState extends State { creator.append("finalResponseAction", finalResponseAction).append("outputMapper", outputMapper); } - /** - * Renders no response. The default implementation. - */ - private class NullFinalResponseAction implements Action { - public Event execute(RequestContext context) { - logger.debug("Not issuing a final response"); - return new Event(this, "success"); - } - - public String toString() { - return "none"; - } - } - - /** - * Maps no output attributes. The default implementation. - */ - private class NoOutputMapper implements AttributeMapper { - public void map(Object source, Object target, MappingContext context) { - } - - public String toString() { - return "none"; - } - } } \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/Flow.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/Flow.java index c02df2b9..c7c410af 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/Flow.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/Flow.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.support.StaticListableBeanFactory; import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.MappingContextImpl; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.core.style.StylerUtils; @@ -511,7 +512,7 @@ public class Flow extends AnnotatedObject implements FlowDefinition { assertStartStateSet(); createVariables(context); if (inputMapper != null) { - inputMapper.map(input, context, null); + inputMapper.map(input, context, new MappingContextImpl(context.getMessageContext())); } startActionList.execute(context); startState.enter(context); @@ -563,7 +564,7 @@ public class Flow extends AnnotatedObject implements FlowDefinition { public void end(RequestControlContext context, MutableAttributeMap output) throws FlowExecutionException { endActionList.execute(context); if (outputMapper != null) { - outputMapper.map(context, output, null); + outputMapper.map(context, output, new MappingContextImpl(context.getMessageContext())); } } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java index dfd78391..00c5b75e 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java @@ -983,6 +983,14 @@ public class XmlFlowBuilder extends AbstractFlowBuilder implements ResourceHolde public void setValue(Object context, Object value) throws EvaluationException { throw new UnsupportedOperationException("Cannot set a subflow expression"); } + + public Class getValueType(Object context) { + return null; + } + + public String getExpressionString() { + return null; + } } public String toString() { diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-2.0.xsd b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-2.0.xsd index feac50ec..d92cbb9e 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-2.0.xsd +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-2.0.xsd @@ -121,6 +121,15 @@ Goes out of scope when the governing conversation ends. + + + + + + + @@ -649,7 +658,21 @@ Handles exceptions that occur within this state. - + + + + + + + + + + + + + @@ -737,7 +760,7 @@ The expected value type. If the actual value type does not match the expected, a diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/AbstractSubflowAttributeMapper.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/AbstractSubflowAttributeMapper.java deleted file mode 100644 index c2f9b2ae..00000000 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/AbstractSubflowAttributeMapper.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.engine.support; - -import java.io.Serializable; - -import org.springframework.binding.mapping.AttributeMapper; -import org.springframework.binding.mapping.MappingContext; -import org.springframework.webflow.core.collection.AttributeMap; -import org.springframework.webflow.core.collection.LocalAttributeMap; -import org.springframework.webflow.core.collection.MutableAttributeMap; -import org.springframework.webflow.engine.SubflowAttributeMapper; -import org.springframework.webflow.execution.RequestContext; - -/** - * Convenient base class for subflow attribute mapper implementations. Encapsulates common attribute mapper workflow. - * Contains no state. Subclasses must override the {@link #getInputMapper()} and {@link #getOutputMapper()} methods to - * return the input mapper and output mapper, respectively. - * - * @author Keith Donald - */ -public abstract class AbstractSubflowAttributeMapper implements SubflowAttributeMapper, Serializable { - - /** - * Returns the input mapper to use to map attributes of a parent flow {@link RequestContext} to a subflow input - * {@link AttributeMap attribute map}. - * @return the input mapper, or null if none - * @see #createFlowInput(RequestContext) - */ - protected abstract AttributeMapper getInputMapper(); - - /** - * Returns the output mapper to use to map attributes from a subflow output {@link AttributeMap attribute map} to - * the {@link RequestContext}. - * @return the output mapper, or null if none - * @see #mapFlowOutput(AttributeMap, RequestContext) - */ - protected abstract AttributeMapper getOutputMapper(); - - public MutableAttributeMap createFlowInput(RequestContext context) { - AttributeMapper inputMapper = getInputMapper(); - if (inputMapper != null) { - LocalAttributeMap input = new LocalAttributeMap(); - inputMapper.map(context, input, getMappingContext(context)); - return input; - } else { - return new LocalAttributeMap(); - } - } - - public void mapFlowOutput(AttributeMap subflowOutput, RequestContext context) { - AttributeMapper outputMapper = getOutputMapper(); - if (getOutputMapper() != null && subflowOutput != null) { - outputMapper.map(subflowOutput, context, getMappingContext(context)); - } - } - - /** - * Returns a map of contextual data available during mapping. This implementation just returns null. - */ - protected MappingContext getMappingContext(RequestContext context) { - return null; - } -} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/GenericSubflowAttributeMapper.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/GenericSubflowAttributeMapper.java index 589b72cd..3bf3bc20 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/GenericSubflowAttributeMapper.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/GenericSubflowAttributeMapper.java @@ -18,14 +18,20 @@ package org.springframework.webflow.engine.support; import java.io.Serializable; import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.MappingContextImpl; import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.engine.SubflowAttributeMapper; +import org.springframework.webflow.execution.RequestContext; /** * Simple flow attribute mapper that holds an input and output mapper strategy. * * @author Keith Donald */ -public final class GenericSubflowAttributeMapper extends AbstractSubflowAttributeMapper implements Serializable { +public final class GenericSubflowAttributeMapper implements SubflowAttributeMapper, Serializable { private final AttributeMapper inputMapper; @@ -41,12 +47,20 @@ public final class GenericSubflowAttributeMapper extends AbstractSubflowAttribut this.outputMapper = outputMapper; } - protected AttributeMapper getInputMapper() { - return inputMapper; + public MutableAttributeMap createFlowInput(RequestContext context) { + if (inputMapper != null) { + LocalAttributeMap input = new LocalAttributeMap(); + inputMapper.map(context, input, new MappingContextImpl(context.getMessageContext())); + return input; + } else { + return new LocalAttributeMap(); + } } - protected AttributeMapper getOutputMapper() { - return outputMapper; + public void mapFlowOutput(AttributeMap subflowOutput, RequestContext context) { + if (outputMapper != null && subflowOutput != null) { + outputMapper.map(subflowOutput, context, new MappingContextImpl(context.getMessageContext())); + } } public String toString() { diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/TransitionExecutingFlowExecutionExceptionHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/TransitionExecutingFlowExecutionExceptionHandler.java index 12438b4a..550adf68 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/TransitionExecutingFlowExecutionExceptionHandler.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/TransitionExecutingFlowExecutionExceptionHandler.java @@ -20,8 +20,6 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.core.JdkVersion; -import org.springframework.core.NestedRuntimeException; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.webflow.engine.ActionList; @@ -137,63 +135,24 @@ public class TransitionExecutingFlowExecutionExceptionHandler implements FlowExe * Find the mapped target state resolver for given exception. Returns null if no mapping can be found * for given exception. Will try all exceptions in the exception cause chain. */ - protected TargetStateResolver getTargetStateResolver(FlowExecutionException e) { - if (JdkVersion.getMajorJavaVersion() == JdkVersion.JAVA_13) { - return getTargetStateResolver13(e); - } else { - return getTargetStateResolver14(e); - } - } - - /** - * Internal getTargetStateResolver implementation for use with JDK 1.3. - */ - private TargetStateResolver getTargetStateResolver13(NestedRuntimeException e) { + protected TargetStateResolver getTargetStateResolver(Throwable e) { TargetStateResolver targetStateResolver; - if (isRootCause13(e)) { + if (isRootCause(e)) { return findTargetStateResolver(e.getClass()); } else { targetStateResolver = (TargetStateResolver) exceptionTargetStateMappings.get(e.getClass()); if (targetStateResolver != null) { return targetStateResolver; } else { - if (e.getCause() instanceof NestedRuntimeException) { - return getTargetStateResolver13((NestedRuntimeException) e.getCause()); - } else { - return null; - } + return getTargetStateResolver(e.getCause()); } } } - /** - * Internal getTargetStateResolver implementation for use with JDK 1.4 or later. - */ - private TargetStateResolver getTargetStateResolver14(Throwable t) { - TargetStateResolver targetStateResolver; - if (isRootCause14(t)) { - return findTargetStateResolver(t.getClass()); - } else { - targetStateResolver = (TargetStateResolver) exceptionTargetStateMappings.get(t.getClass()); - if (targetStateResolver != null) { - return targetStateResolver; - } else { - return getTargetStateResolver14(t.getCause()); - } - } - } - - /** - * Check if given exception is the root of the exception cause chain. For use with JDK 1.3. - */ - private boolean isRootCause13(NestedRuntimeException e) { - return e.getCause() == null; - } - /** * Check if given exception is the root of the exception cause chain. For use with JDK 1.4 or later. */ - private boolean isRootCause14(Throwable t) { + private boolean isRootCause(Throwable t) { return t.getCause() == null; } @@ -214,43 +173,15 @@ public class TransitionExecutingFlowExecutionExceptionHandler implements FlowExe return null; } - /** - * Find the root cause of given throwable. - */ - protected Throwable findRootCause(Throwable t) { - if (JdkVersion.getMajorJavaVersion() == JdkVersion.JAVA_13) { - return findRootCause13(t); - } else { - return findRootCause14(t); - } - } - - /** - * Find the root cause of given throwable. For use on JDK 1.3. - */ - private Throwable findRootCause13(Throwable t) { - if (t instanceof NestedRuntimeException) { - NestedRuntimeException nre = (NestedRuntimeException) t; - Throwable cause = nre.getCause(); - if (cause == null) { - return nre; - } else { - return findRootCause13(cause); - } - } else { - return t; - } - } - /** * Find the root cause of given throwable. For use on JDK 1.4 or later. */ - private Throwable findRootCause14(Throwable e) { + private Throwable findRootCause(Throwable e) { Throwable cause = e.getCause(); if (cause == null) { return e; } else { - return findRootCause14(cause); + return findRootCause(cause); } } diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/BindActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/BindActionTests.java new file mode 100644 index 00000000..5e20a041 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/BindActionTests.java @@ -0,0 +1,92 @@ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.convert.support.DefaultConversionService; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.support.ParserContextImpl; +import org.springframework.binding.message.Severity; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.expression.DefaultExpressionParserFactory; +import org.springframework.webflow.test.MockRequestContext; + +public class BindActionTests extends TestCase { + private ExpressionParser expressionParser = DefaultExpressionParserFactory.getExpressionParser(); + private ConversionService conversionService = new DefaultConversionService(); + + private BindAction action; + + public void testSuccessfulBind() throws Exception { + MockRequestContext context = new MockRequestContext(); + context.getFlowScope().put("bindTarget", new BindBean()); + + Expression target = expressionParser.parseExpression("bindTarget", new ParserContextImpl() + .eval(RequestContext.class)); + action = new BindAction(target, expressionParser, conversionService); + + LocalAttributeMap eventData = new LocalAttributeMap(); + eventData.put("stringProperty", "foo"); + eventData.put("integerProperty", "3"); + Event event = new Event(this, "submit", eventData); + context.setLastEvent(event); + + Event result = action.execute(context); + assertEquals("success", result.getId()); + + BindBean bean = (BindBean) context.getFlowScope().get("bindTarget"); + assertEquals("foo", bean.getStringProperty()); + assertEquals(3, bean.getIntegerProperty()); + } + + public void testBindWithErrors() throws Exception { + MockRequestContext context = new MockRequestContext(); + context.getFlowScope().put("bindTarget", new BindBean()); + + Expression target = expressionParser.parseExpression("bindTarget", new ParserContextImpl() + .eval(RequestContext.class)); + action = new BindAction(target, expressionParser, conversionService); + + LocalAttributeMap eventData = new LocalAttributeMap(); + eventData.put("stringProperty", "foo"); + eventData.put("integerProperty", "malformed"); + Event event = new Event(this, "submit", eventData); + context.setLastEvent(event); + + Event result = action.execute(context); + assertEquals("error", result.getId()); + + BindBean bean = (BindBean) context.getFlowScope().get("bindTarget"); + assertEquals("foo", bean.getStringProperty()); + assertEquals(0, bean.getIntegerProperty()); + assertEquals(1, context.getMessageContext().getMessages().length); + assertEquals("integerProperty", context.getMessageContext().getMessages()[0].getSource()); + assertEquals(Severity.ERROR, context.getMessageContext().getMessages()[0].getSeverity()); + assertEquals("The 'integerProperty' value is the wrong type", context.getMessageContext().getMessages()[0] + .getText()); + } + + public static class BindBean { + private String stringProperty; + private int integerProperty; + + public String getStringProperty() { + return stringProperty; + } + + public void setStringProperty(String stringProperty) { + this.stringProperty = stringProperty; + } + + public int getIntegerProperty() { + return integerProperty; + } + + public void setIntegerProperty(int integerProperty) { + this.integerProperty = integerProperty; + } + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/FlowTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/FlowTests.java index 1b7993dd..3044dcdd 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/engine/FlowTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/FlowTests.java @@ -213,7 +213,8 @@ public class FlowTests extends TestCase { MockRequestControlContext context = new MockRequestControlContext(flow); LocalAttributeMap sessionInput = new LocalAttributeMap(); flow.start(context, sessionInput); - assertFalse(context.getFlowScope().contains("attr")); + assertTrue(context.getFlowScope().contains("attr")); + assertNull(context.getFlowScope().get("attr")); } public void testOnEventNullCurrentState() { diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilderTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilderTests.java index 711e6f59..90d2a48d 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilderTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilderTests.java @@ -3,7 +3,7 @@ package org.springframework.webflow.engine.builder.xml; import junit.framework.TestCase; import org.springframework.beans.factory.support.StaticListableBeanFactory; -import org.springframework.binding.mapping.RequiredMappingException; +import org.springframework.binding.mapping.AttributeMappingException; import org.springframework.core.io.ClassPathResource; import org.springframework.webflow.action.ExternalRedirectAction; import org.springframework.webflow.action.FlowDefinitionRedirectAction; @@ -123,7 +123,7 @@ public class XmlFlowBuilderTests extends TestCase { execution.start(input, context); fail("Should have failed"); } catch (FlowExecutionException e) { - RequiredMappingException me = (RequiredMappingException) e.getRootCause(); + AttributeMappingException me = (AttributeMappingException) e.getCause(); } } @@ -141,7 +141,7 @@ public class XmlFlowBuilderTests extends TestCase { execution.start(input, context); fail("Should have failed"); } catch (FlowExecutionException e) { - RequiredMappingException me = (RequiredMappingException) e.getRootCause(); + AttributeMappingException me = (AttributeMappingException) e.getCause(); } }