Mapper and SpelMapper

git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@1902 50f2f4bb-b051-0410-bef5-90022cba6387
This commit is contained in:
Keith Donald 2009-09-16 15:40:11 +00:00
parent aa86f061f7
commit 7e43aaec3e
6 changed files with 425 additions and 0 deletions

View File

@ -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 <S> the source type mapped from
* @param <T> the target type mapped to
*/
public interface Mapper<S, T> {
/**
* 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);
}

View File

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

View File

@ -0,0 +1,13 @@
package org.springframework.mapping.support;
import java.util.Set;
import org.springframework.expression.EvaluationContext;
interface MappableType {
Set<String> getMappableFields(Object instance);
EvaluationContext getMappingContext(Object instance);
}

View File

@ -0,0 +1,7 @@
package org.springframework.mapping.support;
import org.springframework.core.convert.converter.Converter;
public interface MappingConfiguration {
MappingConfiguration setConverter(Converter converter);
}

View File

@ -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<Object, Object> {
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<Mapping> mappings = new LinkedHashSet<Mapping>();
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<Mapping>();
}
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<Mapping> 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<String> getMappableFields(Object object) {
if (object instanceof Map) {
return MAP_MAPPABLE_TYPE.getMappableFields(object);
} else {
return BEAN_MAPPABLE_TYPE.getMappableFields(object);
}
}
private Set<Mapping> getAutoMappings(Object source) {
if (autoMappingEnabled) {
Set<Mapping> autoMappings = new LinkedHashSet<Mapping>();
Set<String> 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<String> getMappableFields(Object instance) {
Map map = (Map) instance;
LinkedHashSet<String> fields = new LinkedHashSet<String>(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<String> getMappableFields(Object instance) {
// TODO
return null;
}
public EvaluationContext getMappingContext(Object instance) {
return new StandardEvaluationContext(instance);
}
}
}

View File

@ -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<String, Object> source = new HashMap<String, Object>();
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<String, Object> source = new HashMap<String, Object>();
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<String, Object> source = new HashMap<String, Object>();
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<String, Object> source = new HashMap<String, Object>();
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
}
}