From 7e43aaec3e4cb9e67a580c59aea18b1b0e131225 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Wed, 16 Sep 2009 15:40:11 +0000 Subject: [PATCH] Mapper and SpelMapper git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@1902 50f2f4bb-b051-0410-bef5-90022cba6387 --- .../org/springframework/mapping/Mapper.java | 34 +++ .../mapping/MappingException.java | 28 +++ .../mapping/support/MappableType.java | 13 + .../mapping/support/MappingConfiguration.java | 7 + .../mapping/support/SpelMapper.java | 222 ++++++++++++++++++ .../mapping/support/SpelMapperTests.java | 121 ++++++++++ 6 files changed, 425 insertions(+) create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/Mapper.java create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/MappingException.java create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/MappableType.java create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/MappingConfiguration.java create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapper.java create mode 100644 org.springframework.context/src/test/java/org/springframework/mapping/support/SpelMapperTests.java 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..e70a57ab259 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/MappingException.java @@ -0,0 +1,28 @@ +/* + * 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; + +/** + * Base runtime exception for the mapping system. + * @author Keith Donald + */ +public class MappingException extends RuntimeException { + + public MappingException(String message, Throwable cause) { + super(message, cause); + } + +} 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..9cf80922983 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableType.java @@ -0,0 +1,13 @@ +package org.springframework.mapping.support; + +import java.util.Set; + +import org.springframework.expression.EvaluationContext; + +interface MappableType { + + Set getMappableFields(Object instance); + + EvaluationContext getMappingContext(Object instance); + +} \ 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..b0240169052 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingConfiguration.java @@ -0,0 +1,7 @@ +package org.springframework.mapping.support; + +import org.springframework.core.convert.converter.Converter; + +public interface MappingConfiguration { + MappingConfiguration setConverter(Converter 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..7b77defc669 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapper.java @@ -0,0 +1,222 @@ +/* + * 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.Map; +import java.util.Set; + +import org.springframework.context.expression.MapAccessor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.ParseException; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.mapping.Mapper; +import org.springframework.mapping.MappingException; + +/** + * A generic object mapper implementation based on the Spring Expression Language (SpEL). + * @author Keith Donald + */ +public class SpelMapper implements Mapper { + + private static final MappableType MAP_MAPPABLE_TYPE = new MapMappableType(); + + private static final MappableType BEAN_MAPPABLE_TYPE = new BeanMappableType(); + + private static final ExpressionParser expressionParser = new SpelExpressionParser(); + + private Set mappings = new LinkedHashSet(); + + private boolean autoMappingEnabled = true; + + public void setAutoMappingEnabled(boolean autoMappingEnabled) { + this.autoMappingEnabled = autoMappingEnabled; + } + + public MappingConfiguration addMapping(String source, String target) { + Expression sourceExp; + try { + sourceExp = expressionParser.parseExpression(source); + } catch (ParseException e) { + throw new IllegalArgumentException("The mapping source '" + source + + "' is not a parseable value expression", e); + } + Expression targetExp; + try { + targetExp = expressionParser.parseExpression(target); + } catch (ParseException e) { + throw new IllegalArgumentException("The mapping target '" + source + + "' is not a parseable property expression", e); + } + Mapping mapping = new Mapping(sourceExp, targetExp); + if (mappings == null) { + mappings = new LinkedHashSet(); + } + mappings.add(mapping); + return mapping; + } + + public void map(Object source, Object target) throws MappingException { + EvaluationContext sourceContext = getMappingContext(source); + EvaluationContext targetContext = getMappingContext(target); + for (Mapping mapping : mappings) { + mapping.map(sourceContext, targetContext); + } + Set autoMappings = getAutoMappings(source); + for (Mapping mapping : autoMappings) { + mapping.map(sourceContext, targetContext); + } + } + + protected EvaluationContext getMappingContext(Object object) { + if (object instanceof Map) { + return MAP_MAPPABLE_TYPE.getMappingContext(object); + } else { + return BEAN_MAPPABLE_TYPE.getMappingContext(object); + } + } + + protected Set getMappableFields(Object object) { + if (object instanceof Map) { + return MAP_MAPPABLE_TYPE.getMappableFields(object); + } else { + return BEAN_MAPPABLE_TYPE.getMappableFields(object); + } + } + + private Set getAutoMappings(Object source) { + if (autoMappingEnabled) { + Set autoMappings = new LinkedHashSet(); + Set fields = getMappableFields(source); + for (String field : fields) { + if (!explicitlyMapped(field)) { + Expression exp; + try { + exp = expressionParser.parseExpression(field); + } catch (ParseException e) { + throw new IllegalArgumentException("The mapping source '" + source + + "' is not a parseable value expression", e); + } + Mapping mapping = new Mapping(exp, exp); + autoMappings.add(mapping); + } + } + return autoMappings; + } else { + return Collections.emptySet(); + } + } + + private boolean explicitlyMapped(String field) { + for (Mapping mapping : mappings) { + if (mapping.source.getExpressionString().equals(field)) { + return true; + } + } + return false; + } + + private static class Mapping implements MappingConfiguration { + + private Expression source; + + private Expression target; + + private Converter converter; + + public Mapping(Expression source, Expression target) { + this.source = source; + this.target = target; + } + + public MappingConfiguration setConverter(Converter converter) { + this.converter = converter; + return this; + } + + public void map(EvaluationContext sourceContext, EvaluationContext targetContext) throws MappingException { + try { + Object value = source.getValue(sourceContext); + if (converter != null) { + value = converter.convert(value); + } + target.setValue(targetContext, value); + } catch (Exception e) { + throw new MappingException("Could not perform mapping", e); + } + } + + public int hashCode() { + return source.getExpressionString().hashCode() + target.getExpressionString().hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof Mapping)) { + return false; + } + Mapping m = (Mapping) o; + return source.getExpressionString().equals(m.source.getExpressionString()) + && target.getExpressionString().equals(m.source.getExpressionString()); + } + + public String toString() { + return source.getExpressionString() + " -> " + target.getExpressionString(); + } + + } + + static class MapMappableType implements MappableType { + + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + + public Set getMappableFields(Object instance) { + Map map = (Map) instance; + LinkedHashSet fields = new LinkedHashSet(map.size(), 1); + for (Object key : map.keySet()) { + fields.add(key.toString()); + } + return fields; + } + + public EvaluationContext getMappingContext(Object instance) { + StandardEvaluationContext context = new StandardEvaluationContext(instance); + context.addPropertyAccessor(new MapAccessor()); + return context; + } + + } + + static class BeanMappableType implements MappableType { + + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + + public Set getMappableFields(Object instance) { + // TODO + return null; + } + + public EvaluationContext getMappingContext(Object instance) { + return new StandardEvaluationContext(instance); + } + + } + +} 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..5390fb3ee99 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/mapping/support/SpelMapperTests.java @@ -0,0 +1,121 @@ +package org.springframework.mapping.support; + +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.mapping.MappingException; +import org.springframework.mapping.support.SpelMapper; + +public class SpelMapperTests { + + private SpelMapper mapper = new SpelMapper(); + + @Test + public void mapAutomatic() throws MappingException { + 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", "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() throws MappingException { + 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() throws MappingException { + 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); + } + + + + public static class Person { + + private String name; + + private int age; + + private Sport favoriteSport; + + 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 enum Sport { + FOOTBALL, BASKETBALL + } +}