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: *
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();
}
}