From a221fb57185437f232e8aedd3723adf64fe48b4a Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Fri, 16 Oct 2009 22:44:54 +0000 Subject: [PATCH] MapperFactory and MapperBuilder config API --- .../org/springframework/mapping/Mapper.java | 2 +- .../ConverterMappingTargetFactory.java | 41 ++ .../mapping/support/FieldToFieldMapping.java | 77 +++ .../support/FieldToMultiFieldMapping.java | 67 ++ .../mapping/support/MappableTypeFactory.java | 2 +- .../mapping/support/MapperBuilder.java | 144 +++++ .../mapping/support/MapperFactory.java | 58 ++ .../mapping/support/Mapping.java | 103 ---- .../mapping/support/MappingConfiguration.java | 58 -- .../mapping/support/MappingContextHolder.java | 2 +- .../mapping/support/MappingConverter.java | 6 +- .../mapping/support/MappingTargetFactory.java | 2 +- .../support/MultiFieldToFieldMapping.java | 58 ++ .../mapping/support/SpelMapper.java | 148 ++--- .../mapping/support/SpelMapperBuilder.java | 92 +++ .../mapping/support/SpelMapping.java | 96 +-- .../mapping/support/SpelMappingContext.java | 64 ++ .../ui/format/jodatime/DateTimeFormatter.java | 5 +- ...SpelMapperTests.java => MappingTests.java} | 574 +++++++++++------- .../mapping/support/XmlDocumentLoader.java | 96 --- .../springframework/mapping/support/order.xml | 3 - 21 files changed, 1040 insertions(+), 658 deletions(-) create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/ConverterMappingTargetFactory.java create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/FieldToFieldMapping.java create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/FieldToMultiFieldMapping.java create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/MapperBuilder.java create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/MapperFactory.java delete mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/Mapping.java delete 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/MultiFieldToFieldMapping.java create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapperBuilder.java create mode 100644 org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMappingContext.java rename org.springframework.context/src/test/java/org/springframework/mapping/support/{SpelMapperTests.java => MappingTests.java} (52%) delete mode 100644 org.springframework.context/src/test/java/org/springframework/mapping/support/XmlDocumentLoader.java delete mode 100644 org.springframework.context/src/test/java/org/springframework/mapping/support/order.xml 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 index f2d9d7e93e8..7893a40b7b7 100644 --- a/org.springframework.context/src/main/java/org/springframework/mapping/Mapper.java +++ b/org.springframework.context/src/main/java/org/springframework/mapping/Mapper.java @@ -30,6 +30,6 @@ public interface Mapper { * @return the mapped target object * @throws MappingException if the mapping process failed */ - Object map(S source, T target); + T map(S source, T target); } diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/ConverterMappingTargetFactory.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/ConverterMappingTargetFactory.java new file mode 100644 index 00000000000..11a369f6ee6 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/ConverterMappingTargetFactory.java @@ -0,0 +1,41 @@ +/* + * 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.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; + +/** + * Creates a mapping target by calling a converter. + * @author Keith Donald + */ +final class ConverterMappingTargetFactory implements MappingTargetFactory { + + private Converter converter; + + public ConverterMappingTargetFactory(Converter converter) { + this.converter = converter; + } + + public boolean supports(TypeDescriptor targetType) { + return true; + } + + public Object createTarget(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.converter.convert(source); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/FieldToFieldMapping.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/FieldToFieldMapping.java new file mode 100644 index 00000000000..7781acf1f39 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/FieldToFieldMapping.java @@ -0,0 +1,77 @@ +/* + * 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.expression.Expression; + +/** + * A mapping between a source field and a target field. + * @author Keith Donald + */ +final class FieldToFieldMapping implements SpelMapping { + + private final Expression sourceField; + + private final Expression targetField; + + @SuppressWarnings("unchecked") + private final Converter converter; + + public FieldToFieldMapping(Expression sourceField, Expression targetField, Converter converter) { + this.sourceField = sourceField; + this.targetField = targetField; + this.converter = converter; + } + + public String getSourceField() { + return this.sourceField.getExpressionString(); + } + + public String getTargetField() { + return this.targetField.getExpressionString(); + } + + @SuppressWarnings("unchecked") + public void map(SpelMappingContext context) { + try { + Object value = context.getSourceFieldValue(this.sourceField); + if (this.converter != null) { + value = this.converter.convert(value); + } + context.setTargetFieldValue(this.targetField, value); + } catch (Exception e) { + context.addMappingFailure(e); + } + } + + public int hashCode() { + return getSourceField().hashCode() + getTargetField().hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof FieldToFieldMapping)) { + return false; + } + FieldToFieldMapping m = (FieldToFieldMapping) o; + return getSourceField().equals(m.getSourceField()) && getTargetField().equals(m.getTargetField()); + } + + public String toString() { + return "[FieldToFieldMapping<" + getSourceField() + " -> " + getTargetField() + ">]"; + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/FieldToMultiFieldMapping.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/FieldToMultiFieldMapping.java new file mode 100644 index 00000000000..a9c3d6dbb5b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/FieldToMultiFieldMapping.java @@ -0,0 +1,67 @@ +/* + * 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.expression.Expression; +import org.springframework.mapping.Mapper; + +/** + * A mapping between a source field and several target fields. + * @author Keith Donald + */ +final class FieldToMultiFieldMapping implements SpelMapping { + + private final Expression sourceField; + + @SuppressWarnings("unchecked") + private final Mapper targetFieldMapper; + + public FieldToMultiFieldMapping(Expression sourceField, Mapper targetFieldMapper) { + this.sourceField = sourceField; + this.targetFieldMapper = targetFieldMapper; + } + + public String getSourceField() { + return this.sourceField.getExpressionString(); + } + + @SuppressWarnings("unchecked") + public void map(SpelMappingContext context) { + try { + Object value = context.getSourceFieldValue(this.sourceField); + this.targetFieldMapper.map(value, context.getTarget()); + } catch (Exception e) { + context.addMappingFailure(e); + } + } + + public int hashCode() { + return getSourceField().hashCode() + this.targetFieldMapper.hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof FieldToMultiFieldMapping)) { + return false; + } + FieldToMultiFieldMapping m = (FieldToMultiFieldMapping) o; + return getSourceField().equals(m.getSourceField()) && this.targetFieldMapper.equals(m.targetFieldMapper); + } + + public String toString() { + return "[FieldToFieldMapping<" + getSourceField() + " -> " + this.targetFieldMapper + ">]"; + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableTypeFactory.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableTypeFactory.java index df701ee2be1..5ce3eaafdc5 100644 --- a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableTypeFactory.java +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableTypeFactory.java @@ -23,7 +23,7 @@ import java.util.Set; * Call {@link #add(MappableType)} to register. * @author Keith Donald */ -public class MappableTypeFactory { +class MappableTypeFactory { private Set> mappableTypes = new LinkedHashSet>(); diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MapperBuilder.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MapperBuilder.java new file mode 100644 index 00000000000..3af54185fe3 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MapperBuilder.java @@ -0,0 +1,144 @@ +/* + * 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.mapping.Mapper; + +/** + * A fluent interface for configuring a {@link Mapper} between a source type and a target type. + * To use, call one or more of the builder methods on this class, then {@link #getMapper()} to obtain the Mapper instance. + * @author Keith Donald + * @param the source type to map from + * @param the target type to map to + * @see #setAutoMappingEnabled(boolean) + * @see #addMapping(String) + * @see #addMapping(String, Converter) + * @see #addMapping(String, Mapper) + * @see #addMapping(String, String) + * @see #addMapping(String, String, Converter) + * @see #addMapping(Mapper) + * @see #addConverter(Converter) + * @see #getMapper() + */ +public interface MapperBuilder { + + /** + * Sets whether "auto mapping" is enabled. + * When enabled, source and target fields with the same name will automatically be mapped unless an explicit mapping override has been registered. + * Set to false to require explicit registration of all source-to-target mapping rules. + * Default is enabled (true). + * @param autoMappingEnabled auto mapping status + */ + MapperBuilder setAutoMappingEnabled(boolean autoMappingEnabled); + + /** + * Register a mapping between a source field and a target field. + * The source and target field names will be the same value. + * For example, calling addMapping("order") will register a mapping that maps between the order field on the source and the order 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 + * @return this, for configuring additional field mapping options fluently + */ + MapperBuilder addMapping(String field); + + /** + * Register a 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. + * For example, calling addMapping("order") will register a mapping that maps between the order field on the source and the order 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 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 + */ + MapperBuilder addMapping(String field, Converter converter); + + /** + * Register a 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 addMapping("name", firstAndLastNameMapper) might register a mapping that maps the name field on the source to the firstName and lastName 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 + * @return this, for configuring additional field mapping options fluently + */ + MapperBuilder addMapping(String field, Mapper mapper); + + /** + * 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 addMapping("order", "primaryOrder") will register a mapping that maps between the order field on the source and the primaryOrder field on the target. + * @param sourceFieldExpression the source field mapping expression + * @param targetFieldExpression the target field mapping expression + * @return this, for configuring additional field mapping options fluently + */ + MapperBuilder addMapping(String sourceField, String targetField); + + /** + * 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 addMapping("order", "primaryOrder") will register a mapping that maps between the order field on the source and the primaryOrder field on the target. + * @param sourceFieldExpression the source field mapping expression + * @param targetFieldExpression the target field mapping expression + * @return this, for configuring additional field mapping options fluently + */ + MapperBuilder addMapping(String sourceField, String targetField, Converter converter); + + /** + * Register a mapping between multiple source fields and a single target field. + * For example, calling addMapping(dateAndTimeFieldsToDateTimeFieldMapper) might register a mapping that maps the date and time fields on the source to the dateTime 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 mapper the fields to field mapper + * @return this, for configuring additional field mapping options fluently + */ + MapperBuilder addMapping(Mapper mapper); + + /** + * 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. + * The target instance that is mapped is constructed by calling its default constructor. + * @param nestedMapper the nested mapper + * @return this, for configuring additional field mapping options fluently + */ + MapperBuilder addNestedMapper(Mapper nestedMapper); + + /** + * 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. + * The target instance that is mapped is constructed by calling the provided Converter. + * @param nestedMapper the nested mapper + * @param converter the target converter + * @return this, for configuring additional field mapping options fluently + */ + MapperBuilder addNestedMapper(Mapper nestedMapper, Converter converter); + + /** + * 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. + * @see Converter + * @see MappingConverter + */ + MapperBuilder addConverter(Converter converter); + + /** + * Get the Mapper produced by this builder. + * Call this method after instructing the builder. + * @return the Mapper between S and T ready for use + */ + Mapper getMapper(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MapperFactory.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MapperFactory.java new file mode 100644 index 00000000000..b98f1f32c00 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MapperFactory.java @@ -0,0 +1,58 @@ +/* + * 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.mapping.Mapper; + +/** + * Factory for creating general-purpose Mappers without depending on a concrete Mapper implementation class. + * @see #defaultMapper() + * @see #mapperBuilder() + * @see #mapperBuilder(Class, Class) + * @author Keith Donald + */ +public class MapperFactory { + + private static final SpelMapper DEFAULT_MAPPER = new SpelMapper(); + + /** + * Get the default Mapper instance suitable for mapping between most object types using "auto mapping" based on field names. + * The Mapper returned is shared and immutable and should not be downcast & modified. + * @return the default mapper + */ + public static Mapper defaultMapper() { + return DEFAULT_MAPPER; + } + + /** + * Get a builder for a new Mapper instance, allowing customization of object mapping policy. + * @return the MapperBuilder + */ + public static MapperBuilder mapperBuilder() { + return new SpelMapperBuilder(); + } + + /** + * Get a builder for a new Mapper instance that maps between objects of sourceType and targetType. + * Allows for customization of object mapping policy. + * Use this method as an alterntative to {@link #mapperBuilder()} when you'd like more type-safety and validation when configuring and using the Mapper. + * @return the MapperBuilder + */ + public static MapperBuilder mapperBuilder(Class sourceType, Class targetType) { + return new SpelMapperBuilder(sourceType, targetType); + } + +} 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 deleted file mode 100644 index f17fe49773c..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/mapping/support/Mapping.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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; - -/** - * An individual mapping definition between two fields. - * @author Keith Donald - * @since 3.0 - */ -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() + ">]"; - } - - public void setExclude() { - // TODO Auto-generated method stub - } - -} \ 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 deleted file mode 100644 index dacd256168f..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingConfiguration.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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); - - /** - * Configures that this mapping should be excluded (ignored and not executed). - */ - void setExclude(); - -} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingContextHolder.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingContextHolder.java index 42456035f14..2fb39f2a0ea 100644 --- a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingContextHolder.java +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingContextHolder.java @@ -24,7 +24,7 @@ import org.springframework.core.NamedThreadLocal; * @author Keith Donald * @see SpelMapper#map(Object, Object) */ -public abstract class MappingContextHolder { +abstract class MappingContextHolder { private static final ThreadLocal> mappingContextHolder = new NamedThreadLocal>( "Mapping context"); diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingConverter.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingConverter.java index 92f13d87443..d6eae0c4e3c 100644 --- a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingConverter.java +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingConverter.java @@ -27,11 +27,11 @@ import org.springframework.mapping.Mapper; * The default MapperTargetFactory instantiates a target by calling its default constructor. * @author Keith Donald */ -public final class MappingConverter implements GenericConverter { +final class MappingConverter implements GenericConverter { - private Mapper mapper; + private final Mapper mapper; - private MappingTargetFactory mappingTargetFactory; + private final MappingTargetFactory mappingTargetFactory; /** * Creates a new Converter that delegates to the mapper to complete the type conversion process. diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingTargetFactory.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingTargetFactory.java index 1b72dfd39e6..1a71bf55a81 100644 --- a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingTargetFactory.java +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappingTargetFactory.java @@ -25,7 +25,7 @@ import org.springframework.mapping.Mapper; * @see MappingConverter * @see Mapper#map(Object, Object) */ -public interface MappingTargetFactory { +interface MappingTargetFactory { /** * Does this factory support creating mapping targets of the specified type diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MultiFieldToFieldMapping.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MultiFieldToFieldMapping.java new file mode 100644 index 00000000000..7abe0c1dcdd --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MultiFieldToFieldMapping.java @@ -0,0 +1,58 @@ +/* + * 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.mapping.Mapper; + +/** + * A mapping between several source fields and a target field. + * @author Keith Donald + */ +final class MultiFieldToFieldMapping implements SpelMapping { + + @SuppressWarnings("unchecked") + private final Mapper multiFieldMapper; + + public MultiFieldToFieldMapping(Mapper multiFieldMapper) { + this.multiFieldMapper = multiFieldMapper; + } + + @SuppressWarnings("unchecked") + public void map(SpelMappingContext context) { + try { + this.multiFieldMapper.map(context.getSource(), context.getTarget()); + } catch (Exception e) { + context.addMappingFailure(e); + } + } + + public int hashCode() { + return this.multiFieldMapper.hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof MultiFieldToFieldMapping)) { + return false; + } + MultiFieldToFieldMapping m = (MultiFieldToFieldMapping) o; + return this.multiFieldMapper.equals(m.multiFieldMapper); + } + + public String toString() { + return "[MultiFieldToFieldMapping<" + this.multiFieldMapper + ">]"; + } + +} \ 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 index ee12a3caf36..423ece1281d 100644 --- 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 @@ -17,8 +17,6 @@ package org.springframework.mapping.support; import java.util.Collections; import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.List; import java.util.Set; import org.apache.commons.logging.Log; @@ -33,28 +31,19 @@ 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; import org.springframework.util.Assert; /** * A general-purpose object mapper implementation based on the Spring Expression Language (SpEL). * @author Keith Donald - * @see #setAutoMappingEnabled(boolean) - * @see #setMappableTypeFactory(MappableTypeFactory) - * @see #addMapping(String) - * @see #addMapping(String, String) - * @see #addNestedMapper(Mapper) - * @see #addNestedMapper(Mapper, MappingTargetFactory) - * @see #getConverterRegistry() */ -public class SpelMapper implements Mapper { +final class SpelMapper implements Mapper { private static final Log logger = LogFactory.getLog(SpelMapper.class); - private static final SpelExpressionParser sourceExpressionParser = new SpelExpressionParser(); + private final SpelExpressionParser sourceExpressionParser = new SpelExpressionParser(); - private static final SpelExpressionParser targetExpressionParser = new SpelExpressionParser( + private final SpelExpressionParser targetExpressionParser = new SpelExpressionParser( SpelExpressionParserConfiguration.CreateObjectIfAttemptToReferenceNull | SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize); @@ -66,63 +55,35 @@ public class SpelMapper implements Mapper { private MappingConversionService conversionService = new MappingConversionService(); - /** - * Sets whether "auto mapping" is enabled. - * When enabled, source and target fields with the same name will automatically be mapped unless an explicit mapping override has been registered. - * Set to false to require explicit registration of all source-to-target mapping rules. - * Default is enabled (true). - * @param autoMappingEnabled auto mapping status - */ + public SpelMapper() { + + } + + public SpelMapper(Class sourceType, Class targetType) { + // TODO - addMapping assertions based on specified sourceType and targetType + } + public void setAutoMappingEnabled(boolean autoMappingEnabled) { this.autoMappingEnabled = autoMappingEnabled; } - /** - * Sets the factory for {@link MappableType mappable types} supported by this mapper. - * Default is {@link DefaultMappableTypeFactory}. - * @param mappableTypeFactory the mappableTypeFactory - */ public void setMappableTypeFactory(MappableTypeFactory mappableTypeFactory) { this.mappableTypeFactory = mappableTypeFactory; } - /** - * Register a field mapping. - * The source and target field expressions will be the same value. - * For example, calling addMapping("order") will register a mapping that maps between the order field on the source and the order 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 - * @return this, for configuring additional field mapping options fluently - */ - public MappingConfiguration addMapping(String fieldExpression) { - return addMapping(fieldExpression, fieldExpression); + public void addMapping(String sourceFieldExpression, String targetFieldExpression, Converter converter) { + Expression sourceField = parseSourceField(sourceFieldExpression); + Expression targetField = parseTargetField(targetFieldExpression); + FieldToFieldMapping mapping = new FieldToFieldMapping(sourceField, targetField, converter); + this.mappings.add(mapping); } - /** - * Register a mapping between a source and target field. - * For example, calling addMapping("order", "primaryOrder") will register a mapping that maps between the order field on the source and the primaryOrder field on the target. - * @param sourceFieldExpression the source field mapping expression - * @param targetFieldExpression the target field mapping expression - * @return this, for configuring additional field mapping options fluently - */ - public MappingConfiguration addMapping(String sourceFieldExpression, String targetFieldExpression) { - Expression sourceExp; - try { - sourceExp = sourceExpressionParser.parseExpression(sourceFieldExpression); - } catch (ParseException e) { - throw new IllegalArgumentException("The mapping source '" + sourceFieldExpression - + "' is not a parseable value expression", e); - } - Expression targetExp; - try { - targetExp = targetExpressionParser.parseExpression(targetFieldExpression); - } catch (ParseException e) { - throw new IllegalArgumentException("The mapping target '" + targetFieldExpression - + "' is not a parseable property expression", e); - } - SpelMapping mapping = new SpelMapping(sourceExp, targetExp); - this.mappings.add(mapping); - return mapping; + public void addMapping(String field, Mapper mapper) { + this.mappings.add(new FieldToMultiFieldMapping(parseSourceField(field), mapper)); + } + + public void addMapping(Mapper mapper) { + this.mappings.add(new MultiFieldToFieldMapping(mapper)); } /** @@ -174,13 +135,6 @@ public class SpelMapper implements Mapper { targetFactory)); } - /** - * Return this mapper's internal converter registry. - * Allows for registration of simple type Converters in addition to MapperConverters that map entire nested object structures using a Mapper. - * To register the latter, consider using one of the {@link #addNestedMapper(Mapper) addNestedMapper} variants. - * @see Converter - * @see MappingConverter - */ public ConverterRegistry getConverterRegistry() { return conversionService; } @@ -192,17 +146,21 @@ public class SpelMapper implements Mapper { MappingContextHolder.push(source); EvaluationContext sourceContext = getEvaluationContext(source); EvaluationContext targetContext = getEvaluationContext(target); - List failures = new LinkedList(); + SpelMappingContext context = new SpelMappingContext(sourceContext, targetContext); for (SpelMapping mapping : this.mappings) { - doMap(mapping, sourceContext, targetContext, failures); + if (logger.isDebugEnabled()) { + logger.debug(MappingContextHolder.getLevel() + mapping); + } + mapping.map(context); } - Set autoMappings = getAutoMappings(sourceContext, targetContext); + Set autoMappings = getAutoMappings(sourceContext, targetContext); for (SpelMapping mapping : autoMappings) { - doMap(mapping, sourceContext, targetContext, failures); - } - if (!failures.isEmpty()) { - throw new MappingException(failures); + if (logger.isDebugEnabled()) { + logger.debug(MappingContextHolder.getLevel() + mapping + " (auto)"); + } + mapping.map(context); } + context.handleFailures(); return target; } finally { MappingContextHolder.pop(); @@ -211,6 +169,28 @@ public class SpelMapper implements Mapper { // internal helpers + private Expression parseSourceField(String sourceFieldExpression) { + Expression sourceExp; + try { + sourceExp = sourceExpressionParser.parseExpression(sourceFieldExpression); + } catch (ParseException e) { + throw new IllegalArgumentException("The mapping source '" + sourceFieldExpression + + "' is not a parseable value expression", e); + } + return sourceExp; + } + + private Expression parseTargetField(String targetFieldExpression) { + Expression targetExp; + try { + targetExp = 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) { return GenericTypeResolver.resolveTypeArguments(mapper.getClass(), Mapper.class); } @@ -219,17 +199,9 @@ public class SpelMapper implements Mapper { return mappableTypeFactory.getMappableType(object).getEvaluationContext(object, this.conversionService); } - private void doMap(SpelMapping mapping, EvaluationContext sourceContext, EvaluationContext targetContext, - List failures) { - if (logger.isDebugEnabled()) { - logger.debug(MappingContextHolder.getLevel() + mapping); - } - mapping.map(sourceContext, targetContext, failures); - } - - private Set getAutoMappings(EvaluationContext sourceContext, EvaluationContext targetContext) { + private Set getAutoMappings(EvaluationContext sourceContext, EvaluationContext targetContext) { if (this.autoMappingEnabled) { - Set autoMappings = new LinkedHashSet(); + Set autoMappings = new LinkedHashSet(); Set sourceFields = getMappableFields(sourceContext.getRootObject().getValue()); for (String field : sourceFields) { if (!explicitlyMapped(field)) { @@ -249,7 +221,7 @@ public class SpelMapper implements Mapper { } try { if (targetExpression.isWritable(targetContext)) { - autoMappings.add(new SpelMapping(sourceExpression, targetExpression)); + autoMappings.add(new FieldToFieldMapping(sourceExpression, targetExpression, null)); } } catch (EvaluationException e) { @@ -268,11 +240,11 @@ public class SpelMapper implements Mapper { private boolean explicitlyMapped(String field) { for (SpelMapping mapping : this.mappings) { - if (mapping.getSourceExpressionString().startsWith(field)) { + if (mapping instanceof FieldToFieldMapping + && ((FieldToFieldMapping) mapping).getSourceField().startsWith(field)) { return true; } } return false; } - } diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapperBuilder.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapperBuilder.java new file mode 100644 index 00000000000..8f5b356842e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapperBuilder.java @@ -0,0 +1,92 @@ +/* + * 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.mapping.Mapper; + +/** + * MapperBuilder that builds {@link SpelMapper} instances. + * @author Keith Donald + */ +final class SpelMapperBuilder implements MapperBuilder { + + private final SpelMapper mapper; + + public SpelMapperBuilder() { + this.mapper = new SpelMapper(); + } + + public SpelMapperBuilder(Class sourceType, Class targetType) { + this.mapper = new SpelMapper(sourceType, targetType); + } + + public MapperBuilder setAutoMappingEnabled(boolean autoMappingEnabled) { + this.mapper.setAutoMappingEnabled(autoMappingEnabled); + return this; + } + + public MapperBuilder addMapping(String field) { + this.mapper.addMapping(field, field, null); + return this; + } + + public MapperBuilder addMapping(String field, Converter converter) { + this.mapper.addMapping(field, field, converter); + return this; + } + + public MapperBuilder addMapping(String field, Mapper mapper) { + this.mapper.addMapping(field, mapper); + return this; + } + + public MapperBuilder addMapping(String sourceField, String targetField) { + this.mapper.addMapping(sourceField, targetField, null); + return this; + } + + public MapperBuilder addMapping(String sourceField, String targetField, Converter converter) { + this.mapper.addMapping(sourceField, targetField, converter); + return this; + } + + public MapperBuilder addMapping(Mapper mapper) { + this.mapper.addMapping(mapper); + return this; + } + + public MapperBuilder addNestedMapper(Mapper nestedMapper) { + this.mapper.addNestedMapper(nestedMapper); + return this; + } + + public MapperBuilder addNestedMapper(Mapper nestedMapper, Converter converter) { + this.mapper.addNestedMapper(nestedMapper, new ConverterMappingTargetFactory(converter)); + return this; + } + + public MapperBuilder addConverter(Converter converter) { + this.mapper.getConverterRegistry().addConverter(converter); + return this; + } + + @SuppressWarnings("unchecked") + public Mapper getMapper() { + return (Mapper) this.mapper; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapping.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapping.java index af5d6a756ef..3cb57738557 100644 --- a/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapping.java +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMapping.java @@ -15,97 +15,15 @@ */ 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; - /** - * An individual mapping definition between two fields. + * A single {@link SpelMapper} mapping. * @author Keith Donald */ -class SpelMapping implements MappingConfiguration { - - private Expression source; - - private Expression target; - - private GenericConverter converter; - - private boolean exclude; - - public SpelMapping(Expression source, Expression target) { - this.source = source; - this.target = target; - } - - // implementing MappingConfiguration - - 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 setExclude() { - this.exclude = true; - } - - // public methods - - public String getSourceExpressionString() { - return this.source.getExpressionString(); - } - - public String getTargetExpressionString() { - return this.target.getExpressionString(); - } - - public void map(EvaluationContext sourceContext, EvaluationContext targetContext, - Collection failures) { - if (exclude) { - return; - } - 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 SpelMapping)) { - return false; - } - SpelMapping m = (SpelMapping) o; - return getSourceExpressionString().equals(m.getSourceExpressionString()) - && getTargetExpressionString().equals(m.getTargetExpressionString()); - } - - public String toString() { - return "[Mapping<" + getSourceExpressionString() + " -> " + getTargetExpressionString() + ">]"; - } +interface SpelMapping { + /** + * Execute this mapping. + * @param context the mapping context + */ + void map(SpelMappingContext context); } \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMappingContext.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMappingContext.java new file mode 100644 index 00000000000..538b94348a2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/SpelMappingContext.java @@ -0,0 +1,64 @@ +/* + * 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.LinkedList; +import java.util.List; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.mapping.MappingException; +import org.springframework.mapping.MappingFailure; + +final class SpelMappingContext { + + private final EvaluationContext sourceEvaluationContext; + + private final EvaluationContext targetEvaluationContext; + + private final List failures = new LinkedList(); + + public SpelMappingContext(EvaluationContext sourceEvaluationContext, EvaluationContext targetEvaluationContext) { + this.sourceEvaluationContext = sourceEvaluationContext; + this.targetEvaluationContext = targetEvaluationContext; + } + + public Object getSource() { + return this.sourceEvaluationContext.getRootObject().getValue(); + } + + public Object getTarget() { + return this.targetEvaluationContext.getRootObject().getValue(); + } + + public Object getSourceFieldValue(Expression sourceField) { + return sourceField.getValue(this.sourceEvaluationContext); + } + + public void setTargetFieldValue(Expression targetField, Object value) { + targetField.setValue(this.targetEvaluationContext, value); + } + + public void addMappingFailure(Throwable cause) { + this.failures.add(new MappingFailure(cause)); + } + + public void handleFailures() { + if (!this.failures.isEmpty()) { + throw new MappingException(this.failures); + } + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormatter.java index 6a22b855e8f..4700cec1642 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormatter.java @@ -29,11 +29,11 @@ import org.springframework.ui.format.Formatter; public class DateTimeFormatter implements Formatter { private org.joda.time.format.DateTimeFormatter formatter; - + /** * Creates a new {@link DateTimeFormatter} for the given JodaTime formatting pattern. * @param pattern - * @see DateTimeFormat + * @see DateTimeFormat#forPattern(String) */ public DateTimeFormatter(String pattern) { this.formatter = DateTimeFormat.forPattern(pattern); @@ -41,6 +41,7 @@ public class DateTimeFormatter implements Formatter { /** * Creates a new {@link DateTimeFormatter} for the given JodaTime formatter. + * @param formatter the Date Formatter instance */ public DateTimeFormatter(org.joda.time.format.DateTimeFormatter formatter) { this.formatter = formatter; 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/MappingTests.java similarity index 52% rename from org.springframework.context/src/test/java/org/springframework/mapping/support/SpelMapperTests.java rename to org.springframework.context/src/test/java/org/springframework/mapping/support/MappingTests.java index 74e5333c837..824bd77391d 100644 --- a/org.springframework.context/src/test/java/org/springframework/mapping/support/SpelMapperTests.java +++ b/org.springframework.context/src/test/java/org/springframework/mapping/support/MappingTests.java @@ -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 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 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 source = new HashMap(); - 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 source = new HashMap(); @@ -118,8 +98,8 @@ public class SpelMapperTests { Person target = new Person(); - mapper.addMapping("test", "name"); - mapper.addMapping("test", "favoriteSport"); + Mapper 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 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 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 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() { - public String convert(String source) { - return source + " and baz"; - } - }); - mapper.addNestedMapper(NestedDto.class, Nested.class, nestedMapper); + Mapper nestedMapper = MapperFactory.mapperBuilder(NestedDto.class, Nested.class).addMapping( + "foo", new Converter() { + public String convert(String source) { + return source + " and baz"; + } + }).getMapper(); + + Mapper 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() { - 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 nestedMapper = new Mapper() { - 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 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() { - 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() { + Mapper nestedMapper = MapperFactory.mapperBuilder(NestedDto.class, Nested.class).addMapping( + "foo", new Converter() { public String convert(String source) { return source + " and baz"; } - }); - return (Nested) nestedMapper.map(source, nested); - } - }); + }).getMapper(); + + Mapper mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class) + .setAutoMappingEnabled(false).addMapping("nested").addNestedMapper(nestedMapper, + new Converter() { + 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 mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class) + .setAutoMappingEnabled(false).addMapping("nested", new Converter() { + public Nested convert(NestedDto source) { + Mapper nestedMapper = MapperFactory.mapperBuilder(NestedDto.class, + Nested.class).addMapping("foo", new Converter() { + 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 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() { + 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() { + 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() { + 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 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 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() { - public Person convert(String source) { - return new Person(source); - } - }); + Mapper mapper = MapperFactory.mapperBuilder(PersonDto.class, Person.class) + .setAutoMappingEnabled(false).addMapping("friendRankings").addConverter( + new Converter() { + 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() { - public String convert(String source) { - String[] names = source.split(" "); - return names[0] + " P. " + names[1]; - } - }); + Mapper mapper = MapperFactory.mapperBuilder().addMapping("name", + new Converter() { + 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 { - - public boolean isInstance(Object object) { - return object instanceof Element; + public String getFirstName() { + return firstName; } - public Set getFields(Element object) { - NamedNodeMap map = object.getAttributes(); - Set fields = new LinkedHashSet(); - 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; + } + + } } diff --git a/org.springframework.context/src/test/java/org/springframework/mapping/support/XmlDocumentLoader.java b/org.springframework.context/src/test/java/org/springframework/mapping/support/XmlDocumentLoader.java deleted file mode 100644 index 4836ad4168f..00000000000 --- a/org.springframework.context/src/test/java/org/springframework/mapping/support/XmlDocumentLoader.java +++ /dev/null @@ -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(); - } - } - } -} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/mapping/support/order.xml b/org.springframework.context/src/test/java/org/springframework/mapping/support/order.xml deleted file mode 100644 index 51bddfe70c9..00000000000 --- a/org.springframework.context/src/test/java/org/springframework/mapping/support/order.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file