bind action

attribute mapper enhancements
This commit is contained in:
Keith Donald
2008-03-15 01:10:50 +00:00
parent 21d6318484
commit c5340f7f2b
39 changed files with 540 additions and 405 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

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

View File

@@ -0,0 +1,5 @@
package org.springframework.binding.mapping;
public class AttributeMappingException extends RuntimeException {
}

View File

@@ -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() {

View File

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

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.binding.mapping;
import org.springframework.binding.message.MessageContext;
/**
* A context object with two main responsibities:
* <ol>
@@ -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();
}

View File

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