diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index 00213a5c427..7e9cb2f3310 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -572,7 +572,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra } } } catch (InstantiationException e) { - throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name, "Could not instantiate propertyType [" + type.getName() + "] to auto-grow nestd property path"); + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name, "Could not instantiate propertyType [" + type.getName() + "] to auto-grow nested property path"); } catch (IllegalAccessException e) { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name, "Could not instantiate propertyType [" + type.getName() + "] to auto-grow nested property path"); } diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/Mapper.java b/org.springframework.context/src/main/java/org/springframework/mapping/Mapper.java new file mode 100644 index 00000000000..f6b4802183e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/Mapper.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.mapping; + +/** + * Maps between a source and target. + * @author Keith Donald + * @param the source type mapped from + * @param the target type mapped to + */ +public interface Mapper { + + /** + * Map the source to the target. + * @param source the source to map from + * @param target the target to map to + * @throws MappingException if the mapping process failed + */ + void map(S source, T target); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/MappingException.java b/org.springframework.context/src/main/java/org/springframework/mapping/MappingException.java new file mode 100644 index 00000000000..821354fd8a4 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/MappingException.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.mapping; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.Iterator; +import java.util.List; + +/** + * Thrown in a map operation fails. + * @see Mapper#map(Object, Object) + * @author Keith Donald + */ +public class MappingException extends RuntimeException { + + private List mappingFailures; + + public MappingException(List mappingFailures) { + super((String) null); + this.mappingFailures = mappingFailures; + } + + public int getMappingFailureCount() { + return this.mappingFailures.size(); + } + + public List getMappingFailures() { + return this.mappingFailures; + } + + @Override + public String getMessage() { + StringBuilder sb = new StringBuilder(getMappingFailureCount() + " mapping failure(s) occurred:"); + int i = 1; + for (Iterator it = this.mappingFailures.iterator(); it.hasNext(); i++) { + MappingFailure failure = it.next(); + sb.append(" #").append(i + ") ").append(failure.getMessage()); + if (it.hasNext()) { + sb.append(","); + } + } + return sb.toString(); + } + + @Override + public void printStackTrace(PrintStream ps) { + super.printStackTrace(ps); + synchronized (ps) { + ps.println("Failure cause traces:"); + int i = 1; + for (Iterator it = this.mappingFailures.iterator(); it.hasNext(); i++) { + MappingFailure failure = it.next(); + ps.println("- MappingFailure #" + i + " Cause: "); + Throwable t = failure.getCause(); + if (t != null) { + t.printStackTrace(ps); + } else { + ps.println("null"); + } + } + } + } + + @Override + public void printStackTrace(PrintWriter pw) { + super.printStackTrace(pw); + synchronized (pw) { + pw.println("Failure cause traces:"); + int i = 1; + for (Iterator it = this.mappingFailures.iterator(); it.hasNext(); i++) { + MappingFailure failure = it.next(); + pw.println("- MappingFailure #" + i + " Cause: "); + Throwable t = failure.getCause(); + if (t != null) { + t.printStackTrace(pw); + } else { + pw.println("null"); + } + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/MappingFailure.java b/org.springframework.context/src/main/java/org/springframework/mapping/MappingFailure.java new file mode 100644 index 00000000000..f8d57499ef1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/MappingFailure.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.mapping; + +/** + * Indicates an individual mapping failed. + * @author Keith Donald + */ +public class MappingFailure { + + private final Throwable cause; + + /** + * Create a new mapping failure caused by the exception. + * @param cause the failure cause + */ + public MappingFailure(Throwable cause) { + this.cause = cause; + } + + /** + * The failure message. + */ + public String getMessage() { + return getCause().getMessage(); + } + + /** + * The cause of this failure. + */ + public Throwable getCause() { + return cause; + } + + public String toString() { + return "[MappingFailure cause = " + cause + "]"; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/BeanMappableType.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/BeanMappableType.java new file mode 100644 index 00000000000..ea9e574af58 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/BeanMappableType.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.mapping.support; + +import java.beans.PropertyDescriptor; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.BeanUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeConverter; + +class BeanMappableType implements MappableType { + + public Set getFields(Object object) { + Set fields = new LinkedHashSet(); + PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(object.getClass()); + for (PropertyDescriptor descriptor : descriptors) { + String propertyName = descriptor.getName(); + if (propertyName.equals("class")) { + continue; + } + fields.add(descriptor.getName()); + } + return fields; + } + + public EvaluationContext getEvaluationContext(Object instance, ConversionService conversionService) { + StandardEvaluationContext context = new StandardEvaluationContext(instance); + context.setTypeConverter(new StandardTypeConverter(conversionService)); + return context; + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MapMappableType.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MapMappableType.java new file mode 100644 index 00000000000..ec10a5e62db --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MapMappableType.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.mapping.support; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.context.expression.MapAccessor; +import org.springframework.core.convert.ConversionService; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeConverter; + +class MapMappableType implements MappableType> { + + public Set getFields(Map object) { + LinkedHashSet fields = new LinkedHashSet(object.size(), 1); + for (Object key : object.keySet()) { + if (key != null && key instanceof String) { + fields.add((String) key); + } + } + return fields; + } + + public EvaluationContext getEvaluationContext(Map object, + ConversionService conversionService) { + StandardEvaluationContext context = new StandardEvaluationContext(object); + context.setTypeConverter(new StandardTypeConverter(conversionService)); + context.addPropertyAccessor(new MapAccessor()); + return context; + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableType.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableType.java new file mode 100644 index 00000000000..ccaaf43af57 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableType.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.mapping.support; + +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.expression.EvaluationContext; + +/** + * Encapsulates mapping context for a type of object. + * @param the object type + * @author Keith Donald + */ +interface MappableType { + + /** + * The fields of the object that are eligible for mapping, including any nested fields. + */ + Set getFields(T object); + + /** + * A evaluation context for accessing the object. + */ + EvaluationContext getEvaluationContext(T object, ConversionService conversionService); + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/Mapping.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/Mapping.java new file mode 100644 index 00000000000..317bc0fbd2e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/Mapping.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.mapping.support; + +import java.util.Collection; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.support.ConverterFactoryGenericConverter; +import org.springframework.core.convert.support.ConverterGenericConverter; +import org.springframework.core.convert.support.GenericConverter; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.mapping.MappingFailure; + +class Mapping implements MappingConfiguration { + + private Expression source; + + private Expression target; + + private GenericConverter converter; + + public Mapping(Expression source, Expression target) { + this.source = source; + this.target = target; + } + + public String getSourceExpressionString() { + return this.source.getExpressionString(); + } + + public String getTargetExpressionString() { + return this.target.getExpressionString(); + } + + public MappingConfiguration setConverter(Converter converter) { + return setGenericConverter(new ConverterGenericConverter(converter)); + } + + public MappingConfiguration setConverterFactory(ConverterFactory converter) { + return setGenericConverter(new ConverterFactoryGenericConverter(converter)); + } + + public MappingConfiguration setGenericConverter(GenericConverter converter) { + this.converter = converter; + return this; + } + + public void map(EvaluationContext sourceContext, EvaluationContext targetContext, + Collection failures) { + try { + Object value = this.source.getValue(sourceContext); + if (this.converter != null) { + value = this.converter.convert(value, this.source.getValueTypeDescriptor(sourceContext), this.target + .getValueTypeDescriptor(targetContext)); + } + this.target.setValue(targetContext, value); + } catch (Exception e) { + failures.add(new MappingFailure(e)); + } + } + + public int hashCode() { + return getSourceExpressionString().hashCode() + getTargetExpressionString().hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof Mapping)) { + return false; + } + Mapping m = (Mapping) o; + return getSourceExpressionString().equals(m.getSourceExpressionString()) + && getTargetExpressionString().equals(m.getTargetExpressionString()); + } + + public String toString() { + return "[Mapping<" + getSourceExpressionString() + " -> " + getTargetExpressionString() + ">]"; + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingConfiguration.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingConfiguration.java new file mode 100644 index 00000000000..ea5a5a207ae --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.mapping.support; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.support.GenericConverter; + +/** + * A fluent API for configuring a mapping. + * @see SpelMapper#addMapping(String) + * @see SpelMapper#addMapping(String, String) + * @author Keith Donald + */ +public interface MappingConfiguration { + + /** + * Set the type converter to use during this mapping. + * @param converter the converter + * @return this, for call chaining + */ + MappingConfiguration setConverter(Converter converter); + + /** + * Set the type converter factory to use during this mapping. + * @param converter the converter factory + * @return this, for call chaining + */ + MappingConfiguration setConverterFactory(ConverterFactory converterFactory); + + /** + * Set the generic converter to use during this mapping. + * A generic converter allows access to source and target field type descriptors. + * These descriptors provide additional context that can be used during type conversion. + * @param converter the generic converter + * @return this, for call chaining + */ + MappingConfiguration setGenericConverter(GenericConverter converter); + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapper.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapper.java new file mode 100644 index 00000000000..8ad0c30b4bc --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapper.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.mapping.support; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ParseException; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParserConfiguration; +import org.springframework.mapping.Mapper; +import org.springframework.mapping.MappingException; +import org.springframework.mapping.MappingFailure; + +/** + * A generic object mapper implementation based on the Spring Expression Language (SpEL). + * @author Keith Donald + * @see #setAutoMappingEnabled(boolean) + * @see #addMapping(String) + * @see #addMapping(String, String) + */ +public class SpelMapper implements Mapper { + + private static final MappableTypeFactory mappableTypeFactory = new MappableTypeFactory(); + + private static final SpelExpressionParser sourceExpressionParser = new SpelExpressionParser(); + + private static final SpelExpressionParser targetExpressionParser = new SpelExpressionParser( + SpelExpressionParserConfiguration.CreateObjectIfAttemptToReferenceNull + | SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize); + + private final Set mappings = new LinkedHashSet(); + + private boolean autoMappingEnabled = true; + + private final DefaultConversionService conversionService = new DefaultConversionService(); + + public void setAutoMappingEnabled(boolean autoMappingEnabled) { + this.autoMappingEnabled = autoMappingEnabled; + } + + public MappingConfiguration addMapping(String expression) { + return addMapping(expression, expression); + } + + public ConverterRegistry getConverterRegistry() { + return conversionService; + } + + public MappingConfiguration addMapping(String sourceExpression, String targetExpression) { + Expression sourceExp; + try { + sourceExp = sourceExpressionParser.parseExpression(sourceExpression); + } catch (ParseException e) { + throw new IllegalArgumentException("The mapping source '" + sourceExpression + + "' is not a parseable value expression", e); + } + Expression targetExp; + try { + targetExp = targetExpressionParser.parseExpression(targetExpression); + } catch (ParseException e) { + throw new IllegalArgumentException("The mapping target '" + targetExpression + + "' is not a parseable property expression", e); + } + Mapping mapping = new Mapping(sourceExp, targetExp); + this.mappings.add(mapping); + return mapping; + } + + public void map(Object source, Object target) { + EvaluationContext sourceContext = getMappingContext(source); + EvaluationContext targetContext = getMappingContext(target); + List failures = new LinkedList(); + for (Mapping mapping : this.mappings) { + mapping.map(sourceContext, targetContext, failures); + } + Set autoMappings = getAutoMappings(source, target); + for (Mapping mapping : autoMappings) { + mapping.map(sourceContext, targetContext, failures); + } + if (!failures.isEmpty()) { + throw new MappingException(failures); + } + } + + private EvaluationContext getMappingContext(Object object) { + return mappableTypeFactory.getMappableType(object).getEvaluationContext(object, this.conversionService); + } + + private Set getAutoMappings(Object source, Object target) { + if (this.autoMappingEnabled) { + Set autoMappings = new LinkedHashSet(); + Set sourceFields = getMappableFields(source); + Set targetFields = getMappableFields(target); + for (String field : sourceFields) { + if (!explicitlyMapped(field) && targetFields.contains(field)) { + Expression sourceExpression; + Expression targetExpression; + try { + sourceExpression = sourceExpressionParser.parseExpression(field); + } catch (ParseException e) { + throw new IllegalArgumentException("The mapping source '" + field + + "' is not a parseable value expression", e); + } + try { + targetExpression = targetExpressionParser.parseExpression(field); + } catch (ParseException e) { + throw new IllegalArgumentException("The mapping target '" + field + + "' is not a parseable value expression", e); + } + Mapping mapping = new Mapping(sourceExpression, targetExpression); + autoMappings.add(mapping); + } + } + return autoMappings; + } else { + return Collections.emptySet(); + } + } + + private Set getMappableFields(Object object) { + return mappableTypeFactory.getMappableType(object).getFields(object); + } + + private boolean explicitlyMapped(String field) { + for (Mapping mapping : this.mappings) { + if (mapping.getSourceExpressionString().equals(field)) { + return true; + } + } + return false; + } + + private static class MappableTypeFactory { + + private static final MapMappableType MAP_MAPPABLE_TYPE = new MapMappableType(); + + private static final BeanMappableType BEAN_MAPPABLE_TYPE = new BeanMappableType(); + + @SuppressWarnings("unchecked") + public MappableType getMappableType(T object) { + if (object instanceof Map) { + return (MappableType) MAP_MAPPABLE_TYPE; + } else { + return (MappableType) BEAN_MAPPABLE_TYPE; + } + } + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/mapping/support/SpelMapperTests.java b/org.springframework.context/src/test/java/org/springframework/mapping/support/SpelMapperTests.java new file mode 100644 index 00000000000..e8fda5fd7fd --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/mapping/support/SpelMapperTests.java @@ -0,0 +1,370 @@ +package org.springframework.mapping.support; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.springframework.core.convert.converter.Converter; +import org.springframework.mapping.MappingException; + +public class SpelMapperTests { + + private SpelMapper mapper = new SpelMapper(); + + @Test + public void mapAutomatic() { + Map source = new HashMap(); + source.put("name", "Keith"); + source.put("age", 31); + + Person target = new Person(); + + mapper.map(source, target); + + assertEquals("Keith", target.name); + assertEquals(31, target.age); + } + + @Test + public void mapExplicit() throws MappingException { + mapper.setAutoMappingEnabled(false); + mapper.addMapping("name"); + + Map source = new HashMap(); + source.put("name", "Keith"); + source.put("age", 31); + + Person target = new Person(); + + mapper.map(source, target); + + assertEquals("Keith", target.name); + assertEquals(0, target.age); + } + + @Test + public void mapAutomaticWithExplictOverrides() { + mapper.addMapping("test", "age"); + + Map source = new HashMap(); + source.put("name", "Keith"); + source.put("test", "3"); + source.put("favoriteSport", "FOOTBALL"); + + Person target = new Person(); + + mapper.map(source, target); + + assertEquals("Keith", target.name); + assertEquals(3, target.age); + assertEquals(Sport.FOOTBALL, target.favoriteSport); + } + + @Test + public void mapSameSourceFieldToMultipleTargets() { + mapper.addMapping("test", "name"); + mapper.addMapping("test", "favoriteSport"); + + Map source = new HashMap(); + source.put("test", "FOOTBALL"); + + Person target = new Person(); + + mapper.map(source, target); + + assertEquals("FOOTBALL", target.name); + assertEquals(0, target.age); + assertEquals(Sport.FOOTBALL, target.favoriteSport); + } + + @Test + public void mapBean() { + PersonDto source = new PersonDto(); + source.setFullName("Keith Donald"); + source.setAge("31"); + source.setSport("FOOTBALL"); + + Person target = new Person(); + + mapper.addMapping("fullName", "name"); + mapper.addMapping("sport", "favoriteSport"); + + mapper.map(source, target); + + assertEquals("Keith Donald", target.name); + assertEquals(31, target.age); + assertEquals(Sport.FOOTBALL, target.favoriteSport); + } + + @Test + public void mapBeanNested() { + PersonDto source = new PersonDto(); + source.setFullName("Keith Donald"); + source.setAge("31"); + source.setSport("FOOTBALL"); + + Person target = new Person(); + + mapper.addMapping("fullName", "nested.fullName"); + mapper.addMapping("age", "nested.age"); + mapper.addMapping("sport", "nested.sport"); + + mapper.map(source, target); + + assertEquals("Keith Donald", target.getNested().getFullName()); + assertEquals("31", target.nested.age); + assertEquals("FOOTBALL", target.nested.sport); + } + + @Test + public void mapList() { + PersonDto source = new PersonDto(); + List sports = new ArrayList(); + sports.add("FOOTBALL"); + sports.add("BASKETBALL"); + source.setSports(sports); + + Person target = new Person(); + + mapper.setAutoMappingEnabled(false); + mapper.addMapping("sports", "favoriteSports"); + + mapper.map(source, target); + + assertEquals(Sport.FOOTBALL, target.favoriteSports.get(0)); + assertEquals(Sport.BASKETBALL, target.favoriteSports.get(1)); + } + + @Test + public void mapMap() { + PersonDto source = new PersonDto(); + Map friendRankings = new HashMap(); + friendRankings.put("Keri", "1"); + friendRankings.put("Alf", "2"); + source.setFriendRankings(friendRankings); + + Person target = new Person(); + + mapper.setAutoMappingEnabled(false); + mapper.addMapping("friendRankings", "friendRankings"); + mapper.getConverterRegistry().addConverter(new Converter() { + public Person convert(String source) { + return new Person(source); + } + }); + mapper.map(source, target); + + assertEquals(new Integer(1), target.friendRankings.get(new Person("Keri"))); + assertEquals(new Integer(2), target.friendRankings.get(new Person("Alf"))); + } + + @Test + public void mapFieldConverter() { + Map source = new HashMap(); + source.put("name", "Keith Donald"); + source.put("age", 31); + + Person target = new Person(); + + mapper.addMapping("name").setConverter(new Converter() { + public String convert(String source) { + String[] names = source.split(" "); + return names[0] + " P. " + names[1]; + } + }); + + mapper.map(source, target); + + assertEquals("Keith P. Donald", target.name); + assertEquals(31, target.age); + } + + @Test + public void mapFailure() { + Map source = new HashMap(); + source.put("name", "Keith"); + source.put("age", "bogus"); + Person target = new Person(); + try { + mapper.map(source, target); + } catch (MappingException e) { + assertEquals(1, e.getMappingFailureCount()); + } + } + + public static class PersonDto { + + private String fullName; + + private String age; + + private String sport; + + private List sports; + + private Map friendRankings; + + private NestedDto nestedDto; + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getAge() { + return age; + } + + public void setAge(String age) { + this.age = age; + } + + public String getSport() { + return sport; + } + + public void setSport(String sport) { + this.sport = sport; + } + + public List getSports() { + return sports; + } + + public void setSports(List sports) { + this.sports = sports; + } + + public Map getFriendRankings() { + return friendRankings; + } + + public void setFriendRankings(Map friendRankings) { + this.friendRankings = friendRankings; + } + + public NestedDto getNestedDto() { + return nestedDto; + } + + public void setNestedDto(NestedDto nestedDto) { + this.nestedDto = nestedDto; + } + + } + + public static class NestedDto { + + private String foo; + + public String getFoo() { + return foo; + } + } + + public static class Person { + + private String name; + + private int age; + + private Sport favoriteSport; + + private PersonDto nested; + + // private Person cyclic; + + private List favoriteSports; + + private Map friendRankings; + + public Person() { + + } + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Sport getFavoriteSport() { + return favoriteSport; + } + + public void setFavoriteSport(Sport favoriteSport) { + this.favoriteSport = favoriteSport; + } + + public PersonDto getNested() { + return nested; + } + + public void setNested(PersonDto nested) { + this.nested = nested; + } + + /* + public Person getCyclic() { + return cyclic; + } + + public void setCyclic(Person cyclic) { + this.cyclic = cyclic; + } + */ + + public List getFavoriteSports() { + return favoriteSports; + } + + public void setFavoriteSports(List favoriteSports) { + this.favoriteSports = favoriteSports; + } + + public Map getFriendRankings() { + return friendRankings; + } + + public void setFriendRankings(Map friendRankings) { + this.friendRankings = friendRankings; + } + + public int hashCode() { + return name.hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof Person)) { + return false; + } + Person p = (Person) o; + return name.equals(p.name); + } + } + + public enum Sport { + FOOTBALL, BASKETBALL + } +} diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConverterFactoryGenericConverter.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConverterFactoryGenericConverter.java new file mode 100644 index 00000000000..b1d366a7f48 --- /dev/null +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConverterFactoryGenericConverter.java @@ -0,0 +1,25 @@ +/** + * + */ +package org.springframework.core.convert.support; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConverterFactory; + +/** + * Adapts a ConverterFactory to the uniform GenericConverter interface. + * @author Keith Donald + */ +@SuppressWarnings("unchecked") +public final class ConverterFactoryGenericConverter implements GenericConverter { + + private final ConverterFactory converterFactory; + + public ConverterFactoryGenericConverter(ConverterFactory converterFactory) { + this.converterFactory = converterFactory; + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.converterFactory.getConverter(targetType.getObjectType()).convert(source); + } +} \ No newline at end of file diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConverterGenericConverter.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConverterGenericConverter.java new file mode 100644 index 00000000000..c3c2ae9fe3a --- /dev/null +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConverterGenericConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.core.convert.support; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; + +/** + * Adapts a Converter to the uniform GenericConverter interface. + * @author Keith Donald + */ +@SuppressWarnings("unchecked") +public final class ConverterGenericConverter implements GenericConverter { + + private final Converter converter; + + public ConverterGenericConverter(Converter converter) { + this.converter = converter; + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.converter.convert(source); + } +} \ No newline at end of file diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java index a4db183888f..ebebebaecda 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -130,7 +130,7 @@ public class GenericConversionService implements ConversionService, ConverterReg } Class sourceType = typeInfo[0]; Class targetType = typeInfo[1]; - getSourceMap(sourceType).put(targetType, new ConverterAdapter(converter)); + getSourceMap(sourceType).put(targetType, new ConverterGenericConverter(converter)); } public void addConverterFactory(ConverterFactory converterFactory) { @@ -141,7 +141,7 @@ public class GenericConversionService implements ConversionService, ConverterReg } Class sourceType = typeInfo[0]; Class targetType = typeInfo[1]; - getSourceMap(sourceType).put(targetType, new ConverterFactoryAdapter(converterFactory)); + getSourceMap(sourceType).put(targetType, new ConverterFactoryGenericConverter(converterFactory)); } public void removeConvertible(Class sourceType, Class targetType) { @@ -188,7 +188,7 @@ public class GenericConversionService implements ConversionService, ConverterReg return invokeConverter(converter, source, sourceType, targetType); } - + // subclassing hooks /** @@ -242,10 +242,10 @@ public class GenericConversionService implements ConversionService, ConverterReg } } - + // internal helpers - private Class[] getRequiredTypeInfo(Object converter, Class ifc) { + private Class[] getRequiredTypeInfo(Object converter, Class genericIfc) { Class[] typeInfo = new Class[2]; if (converter instanceof ConverterInfo) { ConverterInfo info = (ConverterInfo) converter; @@ -254,7 +254,7 @@ public class GenericConversionService implements ConversionService, ConverterReg return typeInfo; } else { - return GenericTypeResolver.resolveTypeArguments(converter.getClass(), ifc); + return GenericTypeResolver.resolveTypeArguments(converter.getClass(), genericIfc); } } @@ -370,34 +370,4 @@ public class GenericConversionService implements ConversionService, ConverterReg } } - - private static class ConverterAdapter implements GenericConverter { - - private Converter converter; - - public ConverterAdapter(Converter converter) { - this.converter = converter; - } - - @SuppressWarnings("unchecked") - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - return this.converter.convert(source); - } - } - - - private static class ConverterFactoryAdapter implements GenericConverter { - - private ConverterFactory converterFactory; - - public ConverterFactoryAdapter(ConverterFactory converterFactory) { - this.converterFactory = converterFactory; - } - - @SuppressWarnings("unchecked") - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - return this.converterFactory.getConverter(targetType.getObjectType()).convert(source); - } - } - }