conditional mapping

This commit is contained in:
Keith Donald 2009-10-17 05:33:04 +00:00
parent 3c4596a424
commit 9584a81687
11 changed files with 291 additions and 80 deletions

View File

@ -26,6 +26,12 @@ import org.springframework.util.ClassUtils;
*/
final class DefaultMappingTargetFactory implements MappingTargetFactory {
private static final DefaultMappingTargetFactory INSTANCE = new DefaultMappingTargetFactory();
private DefaultMappingTargetFactory() {
}
public boolean supports(TypeDescriptor targetType) {
return ClassUtils.hasConstructor(targetType.getType(), null);
}
@ -34,4 +40,8 @@ final class DefaultMappingTargetFactory implements MappingTargetFactory {
return BeanUtils.instantiate(targetType.getType());
}
public static MappingTargetFactory getInstance() {
return INSTANCE;
}
}

View File

@ -31,10 +31,13 @@ final class FieldToFieldMapping implements SpelMapping {
@SuppressWarnings("unchecked")
private final Converter converter;
public FieldToFieldMapping(Expression sourceField, Expression targetField, Converter<?, ?> converter) {
private final Expression condition;
public FieldToFieldMapping(Expression sourceField, Expression targetField, Converter<?, ?> converter, Expression condition) {
this.sourceField = sourceField;
this.targetField = targetField;
this.converter = converter;
this.condition = condition;
}
public String getSourceField() {
@ -51,6 +54,9 @@ final class FieldToFieldMapping implements SpelMapping {
@SuppressWarnings("unchecked")
public void map(SpelMappingContext context) {
if (!context.conditionHolds(this.condition)) {
return;
}
try {
Object value = context.getSourceFieldValue(this.sourceField);
if (this.converter != null) {

View File

@ -28,10 +28,13 @@ final class FieldToMultiFieldMapping implements SpelMapping {
@SuppressWarnings("unchecked")
private final Mapper targetFieldMapper;
public FieldToMultiFieldMapping(Expression sourceField, Mapper<?, ?> targetFieldMapper) {
private final Expression condition;
public FieldToMultiFieldMapping(Expression sourceField, Mapper<?, ?> targetFieldMapper, Expression condition) {
this.sourceField = sourceField;
this.targetFieldMapper = targetFieldMapper;
this.condition = condition;
}
public String getSourceField() {
@ -44,6 +47,9 @@ final class FieldToMultiFieldMapping implements SpelMapping {
@SuppressWarnings("unchecked")
public void map(SpelMappingContext context) {
if (!context.conditionHolds(this.condition)) {
return;
}
try {
Object value = context.getSourceFieldValue(this.sourceField);
this.targetFieldMapper.map(value, context.getTarget());

View File

@ -50,7 +50,7 @@ public interface MapperBuilder<S, T> {
* The source and target field names will be the same value.
* For example, calling <code>addMapping("order")</code> will register a mapping that maps between the <code>order</code> field on the source and the <code>order</code> field on the target.
* This is a convenience method for calling {@link #addMapping(String, String)} with the same source and target value..
* @param fieldExpression the field mapping expression
* @param field the field mapping expression
* @return this, for configuring additional field mapping options fluently
*/
MapperBuilder<S, T> addMapping(String field);
@ -60,7 +60,7 @@ public interface MapperBuilder<S, T> {
* The source and target field expressions will be the same value.
* For example, calling <code>addMapping("order")</code> will register a mapping that maps between the <code>order</code> field on the source and the <code>order</code> field on the target.
* This is a convenience method for calling {@link #addMapping(String, String, Converter)} with the same source and target value..
* @param fieldExpression the field mapping expression
* @param field the field mapping expression
* @param converter the converter that will convert the source field value before mapping the value to the target field
* @return this, for configuring additional field mapping options fluently
*/
@ -81,8 +81,8 @@ public interface MapperBuilder<S, T> {
* Register a mapping between a source field and a target field.
* Use this method when the name of the source field and the name of the target field are different.
* For example, calling <code>addMapping("order", "primaryOrder")</code> will register a mapping that maps between the <code>order</code> field on the source and the <code>primaryOrder</code> field on the target.
* @param sourceFieldExpression the source field mapping expression
* @param targetFieldExpression the target field mapping expression
* @param sourceField the source field mapping expression
* @param targetField the target field mapping expression
* @return this, for configuring additional field mapping options fluently
*/
MapperBuilder<S, T> addMapping(String sourceField, String targetField);
@ -91,8 +91,8 @@ public interface MapperBuilder<S, T> {
* Register a mapping between a source field and a target field that first converts the source field value using the provided Converter.
* Use this method when the name of the source field and the name of the target field are different.
* For example, calling <code>addMapping("order", "primaryOrder")</code> will register a mapping that maps between the <code>order</code> field on the source and the <code>primaryOrder</code> field on the target.
* @param sourceFieldExpression the source field mapping expression
* @param targetFieldExpression the target field mapping expression
* @param sourceField the source field mapping expression
* @param targetField the target field mapping expression
* @return this, for configuring additional field mapping options fluently
*/
MapperBuilder<S, T> addMapping(String sourceField, String targetField, Converter<?, ?> converter);
@ -107,6 +107,71 @@ public interface MapperBuilder<S, T> {
*/
MapperBuilder<S, T> addMapping(String[] fields, Mapper<S, T> mapper);
/**
* Register a conditional mapping between a source field and a target field.
* The source and target field names will be the same value.
* @param field the field mapping expression
* @param condition the boolean expression that determines if this mapping executes
* @return this, for configuring additional field mapping options fluently
*/
MapperBuilder<S, T> addConditionalMapping(String field, String condition);
/**
* Register a condition mapping between a source field and a target field that first converts the source field value using the provided Converter.
* The source and target field expressions will be the same value.
* @param field the field mapping expression
* @param converter the converter that converts the source field value before mapping
* @param condition the boolean expression that determines if this mapping executes
* @return this, for configuring additional field mapping options fluently
*/
MapperBuilder<S, T> addConditionalMapping(String field, Converter<?, ?> converter, String condition);
/**
* Register a conditional mapping between a source field and multiple target fields.
* Use this method when you need to map a single source field value to multiple fields on the target.
* For example, calling <code>addMapping("name", firstAndLastNameMapper)</code> might register a mapping that maps the <code>name</code> field on the source to the <code>firstName</code> and <code>lastName</code> fields on the target.
* The target field {@link Mapper} will be passed the value of the source field for its source and the target object T for its target.
* @param field the source field expression
* @param mapper the mapper of the target fields
* @param condition the boolean expression that determines if this mapping executes
* @return this, for configuring additional field mapping options fluently
*/
MapperBuilder<S, T> addConditionalMapping(String field, Mapper<?, T> mapper, String condition);
/**
* Register a conditional mapping between a source field and a target field.
* Use this method when the name of the source field and the name of the target field are different.
* For example, calling <code>addMapping("order", "primaryOrder")</code> will register a mapping that maps between the <code>order</code> field on the source and the <code>primaryOrder</code> field on the target.
* @param sourceField the source field mapping expression
* @param targetField the target field mapping expression
* @param condition the boolean expression that determines if this mapping executes
* @return this, for configuring additional field mapping options fluently
*/
MapperBuilder<S, T> addConditionalMapping(String sourceField, String targetField, String condition);
/**
* Register a conditional mapping between a source field and a target field that first converts the source field value using the provided Converter.
* Use this method when the name of the source field and the name of the target field are different.
* For example, calling <code>addMapping("order", "primaryOrder")</code> will register a mapping that maps between the <code>order</code> field on the source and the <code>primaryOrder</code> field on the target.
* @param sourceField the source field mapping expression
* @param targetField the target field mapping expression
* @param converter the converter that converts the source field value before mapping
* @param condition the boolean expression that determines if this mapping executes
* @return this, for configuring additional field mapping options fluently
*/
MapperBuilder<S, T> addConditionalMapping(String sourceField, String targetField, Converter<?, ?> converter, String condition);
/**
* Register a conditional mapping between multiple source fields and a single target field.
* For example, calling <code>addMapping(dateAndTimeFieldsToDateTimeFieldMapper)</code> might register a mapping that maps the <code>date</code> and <code>time</code> fields on the source to the <code>dateTime</code> field on the target.
* The provided {@link Mapper} will be passed the source object S for its source and the target object T for its target.
* @param fields the source field mapping expressions
* @param mapper the fields to field mapper
* @param condition the boolean expression that determines if this mapping executes
* @return this, for configuring additional field mapping options fluently
*/
MapperBuilder<S, T> addMapping(String[] fields, Mapper<S, T> mapper, String condition);
/**
* Register a Mapper that will be used to map between nested source and target fields of a specific sourceType/targetType pair.
* The source and target field types are determined by introspecting the parameterized types on the Mapper generic interface.
@ -130,11 +195,18 @@ public interface MapperBuilder<S, T> {
* Register a custom type converter to use to convert between two mapped types.
* The Converter may convert between simple types, such as Strings to Dates.
* Alternatively, it may convert between complex types and initiate a recursive mapping operation between two object fields.
* @return this, for configuring additional field mapping options fluently
* @see Converter
* @see MappingConverter
*/
MapperBuilder<S, T> addConverter(Converter<?, ?> converter);
/**
* Set the source fields to exclude from mapping.
* @param fields the source fields as var args
*/
MapperBuilder<S, T> setExcludedFields(String... fields);
/**
* Get the Mapper produced by this builder.
* Call this method after instructing the builder.

View File

@ -23,7 +23,7 @@ final class MappingConversionService extends DefaultConversionService {
@Override
protected GenericConverter getDefaultConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
return new MappingConverter(new SpelMapper());
return new MappingConverter(new SpelMapper(), null);
}
}

View File

@ -33,15 +33,6 @@ final class MappingConverter implements GenericConverter {
private final MappingTargetFactory mappingTargetFactory;
/**
* Creates a new Converter that delegates to the mapper to complete the type conversion process.
* Uses a {@link DefaultMappingTargetFactory} to create the target object to map and return.
* @param mapper the mapper
*/
public MappingConverter(Mapper mapper) {
this(mapper, new DefaultMappingTargetFactory());
}
/**
* Creates a new Converter that delegates to the mapper to complete the type conversion process.
* Uses the specified MappingTargetFactory to create the target object to map and return.
@ -49,7 +40,11 @@ final class MappingConverter implements GenericConverter {
*/
public MappingConverter(Mapper mapper, MappingTargetFactory mappingTargetFactory) {
this.mapper = mapper;
this.mappingTargetFactory = mappingTargetFactory;
if (mappingTargetFactory != null) {
this.mappingTargetFactory = mappingTargetFactory;
} else {
this.mappingTargetFactory = DefaultMappingTargetFactory.getInstance();
}
}
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {

View File

@ -16,6 +16,7 @@
package org.springframework.mapping.support;
import org.springframework.core.style.StylerUtils;
import org.springframework.expression.Expression;
import org.springframework.mapping.Mapper;
/**
@ -29,9 +30,12 @@ final class MultiFieldToFieldMapping implements SpelMapping {
@SuppressWarnings("unchecked")
private final Mapper multiFieldMapper;
public MultiFieldToFieldMapping(String[] fields, Mapper<?, ?> multiFieldMapper) {
private Expression condition;
public MultiFieldToFieldMapping(String[] fields, Mapper<?, ?> multiFieldMapper, Expression condition) {
this.fields = fields;
this.multiFieldMapper = multiFieldMapper;
this.condition = condition;
}
public String[] getSourceFields() {
@ -49,6 +53,9 @@ final class MultiFieldToFieldMapping implements SpelMapping {
@SuppressWarnings("unchecked")
public void map(SpelMappingContext context) {
if (!context.conditionHolds(this.condition)) {
return;
}
try {
this.multiFieldMapper.map(context.getSource(), context.getTarget());
} catch (Exception e) {

View File

@ -71,64 +71,32 @@ final class SpelMapper implements Mapper<Object, Object> {
this.mappableTypeFactory = mappableTypeFactory;
}
public void addMapping(String sourceFieldExpression, String targetFieldExpression, Converter<?, ?> converter) {
public void setExcludedFields(String[] fields) {
// TODO
}
public void addMapping(String sourceFieldExpression, String targetFieldExpression, Converter<?, ?> converter,
String condition) {
Expression sourceField = parseSourceField(sourceFieldExpression);
Expression targetField = parseTargetField(targetFieldExpression);
FieldToFieldMapping mapping = new FieldToFieldMapping(sourceField, targetField, converter);
FieldToFieldMapping mapping = new FieldToFieldMapping(sourceField, targetField, converter,
parseCondition(condition));
this.mappings.add(mapping);
}
public void addMapping(String field, Mapper<?, ?> mapper) {
this.mappings.add(new FieldToMultiFieldMapping(parseSourceField(field), mapper));
public void addMapping(String field, Mapper<?, ?> mapper, String condition) {
this.mappings.add(new FieldToMultiFieldMapping(parseSourceField(field), mapper, parseCondition(condition)));
}
public void addMapping(String[] fields, Mapper<?, ?> mapper) {
this.mappings.add(new MultiFieldToFieldMapping(fields, mapper));
public void addMapping(String[] fields, Mapper<?, ?> mapper, String condition) {
this.mappings.add(new MultiFieldToFieldMapping(fields, mapper, parseCondition(condition)));
}
/**
* Adds a Mapper that will map the fields of a nested sourceType/targetType pair.
* The source and target field types are determined by introspecting the parameterized types on the implementation's Mapper generic interface.
* The target instance that is mapped is constructed by a {@link DefaultMappingTargetFactory}.
* This method is a convenience method for {@link #addNestedMapper(Class, Class, Mapper)}.
* @param nestedMapper the nested mapper
*/
public void addNestedMapper(Mapper<?, ?> nestedMapper) {
Class<?>[] typeInfo = getRequiredTypeInfo(nestedMapper);
addNestedMapper(typeInfo[0], typeInfo[1], nestedMapper);
}
/**
* Adds a Mapper that will map the fields of a nested sourceType/targetType pair.
* The source and target field types are determined by introspecting the parameterized types on the implementation's Mapper generic interface.
* The target instance that is mapped is constructed by the provided {@link MappingTargetFactory}.
* This method is a convenience method for {@link #addNestedMapper(Class, Class, Mapper, MappingTargetFactory)}.
* @param nestedMapper the nested mapper
* @param targetFactory the nested mapper's target factory
*/
public void addNestedMapper(Mapper<?, ?> nestedMapper, MappingTargetFactory targetFactory) {
Class<?>[] typeInfo = getRequiredTypeInfo(nestedMapper);
addNestedMapper(typeInfo[0], typeInfo[1], nestedMapper, targetFactory);
}
/**
* Adds a Mapper that will map the fields of a nested sourceType/targetType pair.
* The target instance that is mapped is constructed by a {@link DefaultMappingTargetFactory}.
* @param sourceType the source nested object property type
* @param targetType the target nested object property type
* @param nestedMapper the nested mapper
*/
public void addNestedMapper(Class<?> sourceType, Class<?> targetType, Mapper<?, ?> nestedMapper) {
this.conversionService.addGenericConverter(sourceType, targetType, new MappingConverter(nestedMapper));
}
/**
* Adds a Mapper that will map the fields of a nested sourceType/targetType pair.
* @param sourceType the source nested object property type
* @param targetType the target nested object property type
* @param nestedMapper the nested mapper
* @param targetFactory the nested mapper's target factory
*/
public void addNestedMapper(Class<?> sourceType, Class<?> targetType, Mapper<?, ?> nestedMapper,
MappingTargetFactory targetFactory) {
this.conversionService.addGenericConverter(sourceType, targetType, new MappingConverter(nestedMapper,
@ -170,25 +138,33 @@ final class SpelMapper implements Mapper<Object, Object> {
// internal helpers
private Expression parseSourceField(String sourceFieldExpression) {
Expression sourceExp;
try {
sourceExp = sourceExpressionParser.parseExpression(sourceFieldExpression);
return sourceExpressionParser.parseExpression(sourceFieldExpression);
} catch (ParseException e) {
throw new IllegalArgumentException("The mapping source '" + sourceFieldExpression
+ "' is not a parseable value expression", e);
}
return sourceExp;
}
private Expression parseCondition(String condition) {
if (condition == null) {
return null;
}
try {
return sourceExpressionParser.parseExpression(condition);
} catch (ParseException e) {
throw new IllegalArgumentException("The mapping condition '" + condition
+ "' is not a parseable value expression", e);
}
}
private Expression parseTargetField(String targetFieldExpression) {
Expression targetExp;
try {
targetExp = targetExpressionParser.parseExpression(targetFieldExpression);
return targetExpressionParser.parseExpression(targetFieldExpression);
} catch (ParseException e) {
throw new IllegalArgumentException("The mapping target '" + targetFieldExpression
+ "' is not a parseable property expression", e);
}
return targetExp;
}
private Class<?>[] getRequiredTypeInfo(Mapper<?, ?> mapper) {
@ -221,7 +197,7 @@ final class SpelMapper implements Mapper<Object, Object> {
}
try {
if (targetExpression.isWritable(targetContext)) {
autoMappings.add(new FieldToFieldMapping(sourceExpression, targetExpression, null));
autoMappings.add(new FieldToFieldMapping(sourceExpression, targetExpression, null, null));
}
} catch (EvaluationException e) {
@ -246,4 +222,5 @@ final class SpelMapper implements Mapper<Object, Object> {
}
return false;
}
}

View File

@ -40,37 +40,68 @@ final class SpelMapperBuilder<S, T> implements MapperBuilder<S, T> {
}
public MapperBuilder<S, T> addMapping(String field) {
this.mapper.addMapping(field, field, null);
this.mapper.addMapping(field, field, null, null);
return this;
}
public MapperBuilder<S, T> addMapping(String field, Converter<?, ?> converter) {
this.mapper.addMapping(field, field, converter);
this.mapper.addMapping(field, field, converter, null);
return this;
}
public MapperBuilder<S, T> addMapping(String field, Mapper<?, T> mapper) {
this.mapper.addMapping(field, mapper);
this.mapper.addMapping(field, mapper, null);
return this;
}
public MapperBuilder<S, T> addMapping(String sourceField, String targetField) {
this.mapper.addMapping(sourceField, targetField, null);
this.mapper.addMapping(sourceField, targetField, null, null);
return this;
}
public MapperBuilder<S, T> addMapping(String sourceField, String targetField, Converter<?, ?> converter) {
this.mapper.addMapping(sourceField, targetField, converter);
this.mapper.addMapping(sourceField, targetField, converter, null);
return this;
}
public MapperBuilder<S, T> addMapping(String[] fields, Mapper<S, T> mapper) {
this.mapper.addMapping(fields, mapper);
this.mapper.addMapping(fields, mapper, null);
return this;
}
public MapperBuilder<S, T> addConditionalMapping(String field, String condition) {
this.mapper.addMapping(field, field, null, condition);
return this;
}
public MapperBuilder<S, T> addConditionalMapping(String field, Converter<?, ?> converter, String condition) {
this.mapper.addMapping(field, field, converter, condition);
return this;
}
public MapperBuilder<S, T> addConditionalMapping(String field, Mapper<?, T> mapper, String condition) {
this.mapper.addMapping(field, mapper, condition);
return this;
}
public MapperBuilder<S, T> addConditionalMapping(String sourceField, String targetField, String condition) {
this.mapper.addMapping(sourceField, targetField, null, condition);
return this;
}
public MapperBuilder<S, T> addConditionalMapping(String sourceField, String targetField, Converter<?, ?> converter,
String condition) {
this.mapper.addMapping(sourceField, targetField, converter, condition);
return this;
}
public MapperBuilder<S, T> addMapping(String[] fields, Mapper<S, T> mapper, String condition) {
this.mapper.addMapping(fields, mapper, condition);
return this;
}
public MapperBuilder<S, T> addNestedMapper(Mapper<?, ?> nestedMapper) {
this.mapper.addNestedMapper(nestedMapper);
this.mapper.addNestedMapper(nestedMapper, null);
return this;
}
@ -83,6 +114,11 @@ final class SpelMapperBuilder<S, T> implements MapperBuilder<S, T> {
this.mapper.getConverterRegistry().addConverter(converter);
return this;
}
public MapperBuilder<S, T> setExcludedFields(String... fields) {
this.mapper.setExcludedFields(fields);
return this;
}
@SuppressWarnings("unchecked")
public Mapper<S, T> getMapper() {

View File

@ -43,7 +43,14 @@ final class SpelMappingContext {
public Object getTarget() {
return this.targetEvaluationContext.getRootObject().getValue();
}
public boolean conditionHolds(Expression condition) {
if (condition == null) {
return true;
}
return Boolean.TRUE.equals(condition.getValue(this.sourceEvaluationContext));
}
public Object getSourceFieldValue(Expression sourceField) {
return sourceField.getValue(this.sourceEvaluationContext);
}
@ -61,4 +68,5 @@ final class SpelMappingContext {
throw new MappingException(this.failures);
}
}
}

View File

@ -317,6 +317,45 @@ public class MappingTests {
.getActivationDateTime());
}
@Test
public void conditionalMapping() {
Map<String, String> domestic = new HashMap<String, String>();
domestic.put("international", "false");
domestic.put("areaCode", "205");
domestic.put("prefix", "339");
domestic.put("line", "1234");
domestic.put("countryCode", "whatever");
domestic.put("cityCode", "whatever");
Mapper<Map, PhoneNumber> mapper = MapperFactory.mapperBuilder(Map.class, PhoneNumber.class)
.addConditionalMapping("countryCode", "international == 'true'")
.addConditionalMapping("cityCode", "international == 'true'")
.getMapper();
PhoneNumber number = mapper.map(domestic, new PhoneNumber());
assertEquals("205", number.getAreaCode());
assertEquals("339", number.getPrefix());
assertEquals("1234", number.getLine());
assertNull(number.getCountryCode());
assertNull(number.getCityCode());
Map<String, String> international = new HashMap<String, String>();
international.put("international", "true");
international.put("areaCode", "205");
international.put("prefix", "339");
international.put("line", "1234");
international.put("countryCode", "1");
international.put("cityCode", "2");
PhoneNumber number2 = mapper.map(international, new PhoneNumber());
assertEquals("205", number2.getAreaCode());
assertEquals("339", number2.getPrefix());
assertEquals("1234", number2.getLine());
assertEquals("1", number2.getCountryCode());
assertEquals("2", number2.getCityCode());
}
@Test
public void mapList() {
PersonDto source = new PersonDto();
@ -876,4 +915,59 @@ public class MappingTests {
}
}
public static class PhoneNumber {
private String areaCode;
private String prefix;
private String line;
private String countryCode;
private String cityCode;
public String getAreaCode() {
return areaCode;
}
public void setAreaCode(String areaCode) {
this.areaCode = areaCode;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getLine() {
return line;
}
public void setLine(String line) {
this.line = line;
}
public String getCountryCode() {
return countryCode;
}
public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
public String getCityCode() {
return cityCode;
}
public void setCityCode(String cityCode) {
this.cityCode = cityCode;
}
}
}