MapperFactory and MapperBuilder config API

This commit is contained in:
Keith Donald
2009-10-16 22:44:54 +00:00
parent 5616770a27
commit a221fb5718
21 changed files with 1040 additions and 658 deletions

View File

@@ -7,30 +7,27 @@ import static org.junit.Assert.assertTrue;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
import org.junit.Test;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.ClassPathResource;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.mapping.Mapper;
import org.springframework.mapping.MappingException;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
public class SpelMapperTests {
public class MappingTests {
private SpelMapper mapper = new SpelMapper();
@Test
public void testDefaultMapper() {
EmployeeDto dto = new EmployeeDto();
dto.setFirstName("Keith");
dto.setLastName("Donald");
Employee emp = (Employee) MapperFactory.defaultMapper().map(dto, new Employee());
assertEquals("Keith", emp.getFirstName());
assertEquals("Donald", emp.getLastName());
}
@Test
public void mapAutomatic() {
@@ -40,7 +37,7 @@ public class SpelMapperTests {
Person target = new Person();
mapper.map(source, target);
MapperFactory.defaultMapper().map(source, target);
assertEquals("Keith", target.name);
assertEquals(31, target.age);
@@ -54,8 +51,8 @@ public class SpelMapperTests {
Person target = new Person();
mapper.setAutoMappingEnabled(false);
mapper.addMapping("name");
Mapper<Object, Object> mapper = MapperFactory.mapperBuilder().setAutoMappingEnabled(false).addMapping("name")
.getMapper();
mapper.map(source, target);
assertEquals("Keith", target.name);
@@ -71,7 +68,7 @@ public class SpelMapperTests {
Person target = new Person();
mapper.addMapping("test", "age");
Mapper<Object, Object> mapper = MapperFactory.mapperBuilder().addMapping("test", "age").getMapper();
mapper.map(source, target);
assertEquals("Keith", target.name);
@@ -88,29 +85,12 @@ public class SpelMapperTests {
Person target = new Person();
mapper.map(source, target);
MapperFactory.defaultMapper().map(source, target);
assertEquals("Keith", target.name);
assertEquals(31, target.age);
}
@Test
public void mapAutomaticWithExclusions() {
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.addMapping("test").setExclude();
mapper.map(source, target);
assertEquals("Keith", target.name);
assertEquals(0, target.age);
assertEquals(Sport.FOOTBALL, target.favoriteSport);
}
@Test
public void mapSameSourceFieldToMultipleTargets() {
Map<String, Object> source = new HashMap<String, Object>();
@@ -118,8 +98,8 @@ public class SpelMapperTests {
Person target = new Person();
mapper.addMapping("test", "name");
mapper.addMapping("test", "favoriteSport");
Mapper<Object, Object> mapper = MapperFactory.mapperBuilder().addMapping("test", "name").addMapping("test",
"favoriteSport").getMapper();
mapper.map(source, target);
assertEquals("FOOTBALL", target.name);
@@ -136,8 +116,8 @@ public class SpelMapperTests {
Person target = new Person();
mapper.addMapping("fullName", "name");
mapper.addMapping("sport", "favoriteSport");
Mapper<Object, Object> mapper = MapperFactory.mapperBuilder().addMapping("fullName", "name").addMapping(
"sport", "favoriteSport").getMapper();
mapper.map(source, target);
assertEquals("Keith Donald", target.name);
@@ -155,7 +135,8 @@ public class SpelMapperTests {
Person target = new Person();
mapper.addMapping("nested.foo");
Mapper<PersonDto, Person> mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class).addMapping(
"nested.foo").getMapper();
mapper.map(source, target);
assertEquals("bar", target.nested.foo);
@@ -170,8 +151,8 @@ public class SpelMapperTests {
Person target = new Person();
mapper.setAutoMappingEnabled(false);
mapper.addMapping("nested");
Mapper<PersonDto, Person> mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class)
.setAutoMappingEnabled(false).addMapping("nested").getMapper();
mapper.map(source, target);
assertEquals("bar", target.nested.foo);
@@ -186,57 +167,21 @@ public class SpelMapperTests {
Person target = new Person();
SpelMapper nestedMapper = new SpelMapper();
nestedMapper.setAutoMappingEnabled(false);
nestedMapper.addMapping("foo").setConverter(new Converter<String, String>() {
public String convert(String source) {
return source + " and baz";
}
});
mapper.addNestedMapper(NestedDto.class, Nested.class, nestedMapper);
Mapper<NestedDto, Nested> nestedMapper = MapperFactory.mapperBuilder(NestedDto.class, Nested.class).addMapping(
"foo", new Converter<String, String>() {
public String convert(String source) {
return source + " and baz";
}
}).getMapper();
Mapper<PersonDto, Person> mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class)
.setAutoMappingEnabled(false).addMapping("nested").addNestedMapper(nestedMapper).getMapper();
mapper.setAutoMappingEnabled(false);
mapper.addMapping("nested");
mapper.map(source, target);
assertEquals("bar and baz", target.nested.foo);
}
@Test
public void mapBeanNestedCustomNestedMapperCustomMappingTargetFactory() {
PersonDto source = new PersonDto();
final NestedDto nested = new NestedDto();
nested.foo = "bar";
source.setNested(nested);
Person target = new Person();
SpelMapper nestedMapper = new SpelMapper();
nestedMapper.setAutoMappingEnabled(false);
nestedMapper.addMapping("foo").setConverter(new Converter<String, String>() {
public String convert(String source) {
return source + " and baz";
}
});
mapper.addNestedMapper(NestedDto.class, Nested.class, nestedMapper, new MappingTargetFactory() {
public boolean supports(TypeDescriptor targetType) {
return true;
}
public Object createTarget(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
NestedDto nestedDto = (NestedDto) source;
assertEquals(nested, nestedDto);
return new Nested();
}
});
mapper.setAutoMappingEnabled(false);
mapper.addMapping("nested");
mapper.map(source, target);
assertEquals("bar and baz", target.nested.foo);
}
@Test
public void mapBeanNestedCustomNestedMapperHandCoded() {
PersonDto source = new PersonDto();
@@ -247,52 +192,130 @@ public class SpelMapperTests {
Person target = new Person();
Mapper<NestedDto, Nested> nestedMapper = new Mapper<NestedDto, Nested>() {
public Object map(NestedDto source, Nested target) {
target.foo = source.foo + " and baz";
public Nested map(NestedDto source, Nested target) {
target.setFoo(source.getFoo() + " and baz");
return target;
}
};
mapper.addNestedMapper(nestedMapper);
mapper.setAutoMappingEnabled(false);
mapper.addMapping("nested");
Mapper<PersonDto, Person> mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class)
.setAutoMappingEnabled(false).addMapping("nested").addNestedMapper(nestedMapper).getMapper();
mapper.map(source, target);
assertEquals("bar and baz", target.nested.foo);
}
@Test
public void mapBeanNestedCustomConverterDelegatingToMapper() {
public void mapBeanNestedCustomNestedMapperConverterAsTargetFactory() {
PersonDto source = new PersonDto();
NestedDto nested = new NestedDto();
final NestedDto nested = new NestedDto();
nested.foo = "bar";
source.setNested(nested);
Person target = new Person();
mapper.getConverterRegistry().addConverter(new Converter<NestedDto, Nested>() {
public Nested convert(NestedDto source) {
// allows construction of target to be controlled by the converter
Nested nested = new Nested();
// mapping can do whatever, here we delegate to nested SpelMapper
SpelMapper nestedMapper = new SpelMapper();
nestedMapper.addMapping("foo").setConverter(new Converter<String, String>() {
Mapper<NestedDto, Nested> nestedMapper = MapperFactory.mapperBuilder(NestedDto.class, Nested.class).addMapping(
"foo", new Converter<String, String>() {
public String convert(String source) {
return source + " and baz";
}
});
return (Nested) nestedMapper.map(source, nested);
}
});
}).getMapper();
Mapper<PersonDto, Person> mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class)
.setAutoMappingEnabled(false).addMapping("nested").addNestedMapper(nestedMapper,
new Converter<NestedDto, Nested>() {
public Nested convert(NestedDto source) {
assertEquals(nested, source);
return new Nested();
}
}).getMapper();
mapper.setAutoMappingEnabled(false);
mapper.addMapping("nested");
mapper.map(source, target);
assertEquals("bar and baz", target.nested.foo);
}
@Test
public void mapBeanNestedCustomConverterDelegatingToMapper() {
PersonDto source = new PersonDto();
final NestedDto nested = new NestedDto();
nested.foo = "bar";
source.setNested(nested);
Person target = new Person();
Mapper<PersonDto, Person> mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class)
.setAutoMappingEnabled(false).addMapping("nested", new Converter<NestedDto, Nested>() {
public Nested convert(NestedDto source) {
Mapper<NestedDto, Nested> nestedMapper = MapperFactory.mapperBuilder(NestedDto.class,
Nested.class).addMapping("foo", new Converter<String, String>() {
public String convert(String source) {
return source + " and baz";
}
}).getMapper();
return nestedMapper.map(source, new Nested());
}
}).getMapper();
mapper.map(source, target);
assertEquals("bar and baz", target.nested.foo);
}
@Test
public void testCustomMapper() {
Mapper<CreateAccountDto, Account> mapper = MapperFactory.mapperBuilder(CreateAccountDto.class, Account.class)
.setAutoMappingEnabled(false)
// field to field of different name
.addMapping("accountNumber", "number")
// field to multiple fields
.addMapping("name", new Mapper<String, Account>() {
public Account map(String name, Account account) {
String[] names = name.split(" ");
account.setFirstName(names[0]);
account.setLastName(names[1]);
return account;
}
})
// field to field with type conversion
.addMapping("address", new Converter<String, Address>() {
public Address convert(String address) {
String[] fields = address.split(" ");
Address addr = new Address();
addr.setStreet(fields[0]);
addr.setCity(fields[1]);
addr.setState(fields[2]);
addr.setZip(fields[3]);
return addr;
}
})
// multiple fields to field
.addMapping(new Mapper<CreateAccountDto, Account>() {
public Account map(CreateAccountDto source, Account target) {
DateTime dateTime = ISODateTimeFormat.dateTime().parseDateTime(
source.getActivationDate() + "T" + source.getActivationTime());
target.setActivationDateTime(dateTime);
return target;
}
}).getMapper();
CreateAccountDto dto = new CreateAccountDto();
dto.setAccountNumber("123456789");
dto.setName("Keith Donald");
dto.setActivationDate("2009-10-12");
dto.setActivationTime("12:00:00.000Z");
dto.setAddress("2009BelAireEstates PalmBay FL 35452");
Account account = mapper.map(dto, new Account());
assertEquals("Keith", account.getFirstName());
assertEquals("Donald", account.getLastName());
assertEquals("2009BelAireEstates", account.getAddress().getStreet());
assertEquals("PalmBay", account.getAddress().getCity());
assertEquals("FL", account.getAddress().getState());
assertEquals("35452", account.getAddress().getZip());
assertEquals(ISODateTimeFormat.dateTime().parseDateTime("2009-10-12T12:00:00.000Z"), account
.getActivationDateTime());
}
@Test
public void mapList() {
PersonDto source = new PersonDto();
@@ -303,8 +326,8 @@ public class SpelMapperTests {
Person target = new Person();
mapper.setAutoMappingEnabled(false);
mapper.addMapping("sports", "favoriteSports");
Mapper<PersonDto, Person> mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class)
.setAutoMappingEnabled(false).addMapping("sports", "favoriteSports").getMapper();
mapper.map(source, target);
assertEquals(Sport.FOOTBALL, target.favoriteSports.get(0));
@@ -321,8 +344,8 @@ public class SpelMapperTests {
Person target = new Person();
mapper.setAutoMappingEnabled(false);
mapper.addMapping("sports[0]", "favoriteSport");
Mapper<PersonDto, Person> mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class)
.setAutoMappingEnabled(false).addMapping("sports[0]", "favoriteSport").getMapper();
mapper.map(source, target);
assertEquals(Sport.FOOTBALL, target.favoriteSport);
@@ -339,13 +362,15 @@ public class SpelMapperTests {
Person target = new Person();
mapper.setAutoMappingEnabled(false);
mapper.addMapping("friendRankings", "friendRankings");
mapper.getConverterRegistry().addConverter(new Converter<String, Person>() {
public Person convert(String source) {
return new Person(source);
}
});
Mapper<PersonDto, Person> mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class)
.setAutoMappingEnabled(false).addMapping("friendRankings").addConverter(
new Converter<String, Person>() {
public Person convert(String source) {
return new Person(source);
}
}).getMapper();
mapper.map(source, target);
mapper.map(source, target);
assertEquals(new Integer(1), target.friendRankings.get(new Person("Keri")));
@@ -360,12 +385,13 @@ public class SpelMapperTests {
Person target = new Person();
mapper.addMapping("name").setConverter(new Converter<String, String>() {
public String convert(String source) {
String[] names = source.split(" ");
return names[0] + " P. " + names[1];
}
});
Mapper<Object, Object> mapper = MapperFactory.mapperBuilder().addMapping("name",
new Converter<String, String>() {
public String convert(String source) {
String[] names = source.split(" ");
return names[0] + " P. " + names[1];
}
}).getMapper();
mapper.map(source, target);
assertEquals("Keith P. Donald", target.name);
@@ -379,7 +405,7 @@ public class SpelMapperTests {
source.put("age", "invalid");
Person target = new Person();
try {
mapper.map(source, target);
MapperFactory.defaultMapper().map(source, target);
} catch (MappingException e) {
assertEquals(1, e.getMappingFailureCount());
}
@@ -393,7 +419,7 @@ public class SpelMapperTests {
source.setFavoriteSport(Sport.FOOTBALL);
source.cyclic = source;
Person target = new Person();
mapper.map(source, target);
MapperFactory.defaultMapper().map(source, target);
assertEquals("Keith", target.getName());
assertEquals(3, target.getAge());
assertEquals(Sport.FOOTBALL, target.getFavoriteSport());
@@ -411,133 +437,209 @@ public class SpelMapperTests {
Order target = new Order();
mapper.map(source, target);
MapperFactory.defaultMapper().map(source, target);
assertEquals(1, target.getNumber());
assertTrue(item != target.getLineItem());
assertEquals(new BigDecimal("30.00"), target.getLineItem().getAmount());
assertEquals(source, target.getLineItem().getOrder());
}
@Test
public void mapCustomMappableType() throws Exception {
XmlDocumentLoader loader = new XmlDocumentLoader();
loader.setValidating(false);
public static class EmployeeDto {
Object source = loader.loadDocument(new ClassPathResource("order.xml", getClass())).getDocumentElement();
Order target = new Order();
private String firstName;
MappableTypeFactory factory = new MappableTypeFactory();
factory.add(new ElementMappableType());
factory.add(new BeanMappableType());
mapper.setMappableTypeFactory(factory);
mapper.map(source, target);
private String lastName;
assertEquals(1, target.getNumber());
}
static class ElementMappableType implements MappableType<Element> {
public boolean isInstance(Object object) {
return object instanceof Element;
public String getFirstName() {
return firstName;
}
public Set<String> getFields(Element object) {
NamedNodeMap map = object.getAttributes();
Set<String> fields = new LinkedHashSet<String>();
for (int i = 0; i < map.getLength(); i++) {
fields.add(map.item(i).getNodeName());
}
return fields;
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public EvaluationContext getEvaluationContext(Element object, ConversionService conversionService) {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(object);
context.setTypeConverter(new StandardTypeConverter(conversionService));
context.addPropertyAccessor(new PropertyAccessor() {
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
Element e = (Element) target;
return e.hasAttribute(name);
}
public String getLastName() {
return lastName;
}
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
return canRead(context, target, name);
}
public Class<?>[] getSpecificTargetClasses() {
return new Class[] { Element.class };
}
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
Element e = (Element) target;
return new TypedValue(e.getAttribute(name));
}
public void write(EvaluationContext context, Object target, String name, Object newValue)
throws AccessException {
Element e = (Element) target;
e.setAttribute(name, (String) newValue);
}
});
return context;
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
@Test(expected = IllegalArgumentException.class)
public void mapCustomMappableTypeNotSupported() throws Exception {
Order source = new Order();
Order target = new Order();
public static class Employee {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
MappableTypeFactory factory = new MappableTypeFactory();
mapper.setMappableTypeFactory(factory);
mapper.map(source, target);
}
public static class Order {
public static class CreateAccountDto {
private int number;
private String accountNumber;
private LineItem lineItem;
private String name;
public int getNumber() {
private String address;
private String activationDate;
private String activationTime;
public String getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getActivationDate() {
return activationDate;
}
public void setActivationDate(String activationDate) {
this.activationDate = activationDate;
}
public String getActivationTime() {
return activationTime;
}
public void setActivationTime(String activationTime) {
this.activationTime = activationTime;
}
}
public static class Account {
private String number;
private String firstName;
private String lastName;
private Address address;
private DateTime activationDateTime;
public String getNumber() {
return number;
}
public void setNumber(int number) {
public void setNumber(String number) {
this.number = number;
}
public LineItem getLineItem() {
return lineItem;
public String getFirstName() {
return firstName;
}
public void setLineItem(LineItem lineItem) {
this.lineItem = lineItem;
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public DateTime getActivationDateTime() {
return activationDateTime;
}
public void setActivationDateTime(DateTime activationDateTime) {
this.activationDateTime = activationDateTime;
}
}
public static class LineItem {
public static class Address {
private BigDecimal amount;
private String street;
private Order order;
private String city;
public BigDecimal getAmount() {
return amount;
private String state;
private String zip;
public String getStreet() {
return street;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
public void setStreet(String street) {
this.street = street;
}
public Order getOrder() {
return order;
public String getCity() {
return city;
}
public void setOrder(Order order) {
this.order = order;
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
}
@@ -725,4 +827,52 @@ public class SpelMapperTests {
public enum Sport {
FOOTBALL, BASKETBALL
}
public static class Order {
private int number;
private LineItem lineItem;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public LineItem getLineItem() {
return lineItem;
}
public void setLineItem(LineItem lineItem) {
this.lineItem = lineItem;
}
}
public static class LineItem {
private BigDecimal amount;
private Order order;
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
}
}

View File

@@ -1,96 +0,0 @@
package org.springframework.mapping.support;
import java.io.IOException;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.util.xml.SimpleSaxErrorHandler;
import org.w3c.dom.Document;
import org.xml.sax.EntityResolver;
import org.xml.sax.SAXException;
public class XmlDocumentLoader {
private static final Log logger = LogFactory.getLog(XmlDocumentLoader.class);
/**
* JAXP attribute used to configure the schema language for validation.
*/
private static final String SCHEMA_LANGUAGE_ATTRIBUTE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
/**
* JAXP attribute value indicating the XSD schema language.
*/
private static final String XSD_SCHEMA_LANGUAGE = "http://www.w3.org/2001/XMLSchema";
/**
* Flag indicating if the XML document parser will perform schema validation.
*/
private boolean validating = true;
/**
* The spring-webflow schema resolution strategy.
*/
private EntityResolver entityResolver;
/**
* Returns whether or not the XML parser will validate the document.
*/
public boolean isValidating() {
return validating;
}
/**
* Set if the XML parser should validate the document and thus enforce a schema. Defaults to true.
*/
public void setValidating(boolean validating) {
this.validating = validating;
}
/**
* Returns the SAX entity resolver used by the XML parser.
*/
public EntityResolver getEntityResolver() {
return entityResolver;
}
/**
* Set a SAX entity resolver to be used for parsing. Can be overridden for custom entity resolution, for example
* relative to some specific base path.
*/
public void setEntityResolver(EntityResolver entityResolver) {
this.entityResolver = entityResolver;
}
public Document loadDocument(Resource resource) throws IOException, ParserConfigurationException, SAXException {
InputStream is = null;
try {
is = resource.getInputStream();
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(isValidating());
factory.setNamespaceAware(true);
try {
factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
} catch (IllegalArgumentException ex) {
throw new IllegalStateException("Unable to validate using XSD: Your JAXP provider [" + factory
+ "] does not support XML Schema. "
+ "Are you running on Java 1.4 or below with Apache Crimson? "
+ "If so you must upgrade to Apache Xerces (or Java 5 or >) for full XSD support.");
}
DocumentBuilder docBuilder = factory.newDocumentBuilder();
docBuilder.setErrorHandler(new SimpleSaxErrorHandler(logger));
docBuilder.setEntityResolver(getEntityResolver());
return docBuilder.parse(is);
} finally {
if (is != null) {
is.close();
}
}
}
}

View File

@@ -1,3 +0,0 @@
<order number="1">
<lineitem amount="30.00" />
</order>