diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/BeanMappableType.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/BeanMappableType.java index e232bef3868..57009c664ce 100644 --- a/org.springframework.context/src/main/java/org/springframework/mapping/support/BeanMappableType.java +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/BeanMappableType.java @@ -17,6 +17,7 @@ package org.springframework.mapping.support; import java.beans.PropertyDescriptor; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import org.springframework.beans.BeanUtils; @@ -31,7 +32,13 @@ final class BeanMappableType implements MappableType { return true; } - public Set getFields(Object object) { + public EvaluationContext getEvaluationContext(Object object, ConversionService conversionService) { + StandardEvaluationContext context = new StandardEvaluationContext(object); + context.setTypeConverter(new StandardTypeConverter(conversionService)); + return context; + } + + public Set getFieldNames(Object object) { Set fields = new LinkedHashSet(); PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(object.getClass()); for (PropertyDescriptor descriptor : descriptors) { @@ -44,10 +51,9 @@ final class BeanMappableType implements MappableType { return fields; } - public EvaluationContext getEvaluationContext(Object object, ConversionService conversionService) { - StandardEvaluationContext context = new StandardEvaluationContext(object); - context.setTypeConverter(new StandardTypeConverter(conversionService)); - return context; + public Map getNestedFields(String fieldName, Object object) { + return null; } + } \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/FlexibleFieldMapping.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/FlexibleFieldMapping.java new file mode 100644 index 00000000000..3bbd4b8478c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/FlexibleFieldMapping.java @@ -0,0 +1,82 @@ +/* + * 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.style.StylerUtils; +import org.springframework.expression.Expression; +import org.springframework.mapping.Mapper; + +/** + * A mapping between several source fields and one or more target fields. + * @author Keith Donald + */ +final class FlexibleFieldMapping implements SpelMapping { + + private String[] fields; + + @SuppressWarnings("unchecked") + private final Mapper flexibleFieldMapper; + + private Expression condition; + + public FlexibleFieldMapping(String[] fields, Mapper flexibleFieldMapper, Expression condition) { + this.fields = fields; + this.flexibleFieldMapper = flexibleFieldMapper; + this.condition = condition; + } + + public String[] getSourceFields() { + return fields; + } + + public boolean mapsField(String field) { + for (String f : this.fields) { + if (f.equals(field)) { + return true; + } + } + return false; + } + + @SuppressWarnings("unchecked") + public void map(SpelMappingContext context) { + if (!context.conditionHolds(this.condition)) { + return; + } + try { + this.flexibleFieldMapper.map(context.getSource(), context.getTarget()); + } catch (Exception e) { + context.addMappingFailure(e); + } + } + + public int hashCode() { + return this.flexibleFieldMapper.hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof FlexibleFieldMapping)) { + return false; + } + FlexibleFieldMapping m = (FlexibleFieldMapping) o; + return this.flexibleFieldMapper.equals(m.flexibleFieldMapper); + } + + public String toString() { + return "[FlexibleFieldMapping<" + StylerUtils.style(this.fields) + " -> " + this.flexibleFieldMapper + ">]"; + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MapMappableType.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MapMappableType.java index aecec1e91b9..88833bdf46b 100644 --- a/org.springframework.context/src/main/java/org/springframework/mapping/support/MapMappableType.java +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MapMappableType.java @@ -15,6 +15,7 @@ */ package org.springframework.mapping.support; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -31,16 +32,6 @@ final class MapMappableType implements MappableType; } - public Set getFields(Map object) { - LinkedHashSet fields = new LinkedHashSet(object.size(), 1); - for (Object key : object.keySet()) { - if (key != null && key instanceof String) { - fields.add((String) key); - } - } - return fields; - } - public EvaluationContext getEvaluationContext(Map object, ConversionService conversionService) { StandardEvaluationContext context = new StandardEvaluationContext(object); @@ -49,4 +40,28 @@ final class MapMappableType implements MappableType getFieldNames(Map object) { + Set fieldNames = new LinkedHashSet(object.size(), 1); + for (Object key : object.keySet()) { + if (key != null && key instanceof String) { + fieldNames.add((String) key); + } + } + return fieldNames; + } + + public Map getNestedFields(String fieldName, Map object) { + Map fields = new LinkedHashMap(object.size(), 1); + for (Map.Entry entry : object.entrySet()) { + Object key = entry.getKey(); + if (key != null && key instanceof String) { + String name = (String) key; + if (name.startsWith(fieldName + ".")) { + fields.put(name.substring(fieldName.length() + 1), entry.getValue()); + } + } + } + return fields; + } + } \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableType.java b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableType.java index dc3e7758a9f..ee970bad5f0 100644 --- a/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableType.java +++ b/org.springframework.context/src/main/java/org/springframework/mapping/support/MappableType.java @@ -15,6 +15,7 @@ */ package org.springframework.mapping.support; +import java.util.Map; import java.util.Set; import org.springframework.core.convert.ConversionService; @@ -28,9 +29,10 @@ import org.springframework.expression.EvaluationContext; interface MappableType { /** - * The fields of the object that are eligible for mapping, including any nested fields. + * Is this object to map an instanceof this mappable type? + * @param object the object to test */ - Set getFields(T object); + boolean isInstance(Object object); /** * A evaluation context for accessing the object. @@ -38,9 +40,13 @@ interface MappableType { EvaluationContext getEvaluationContext(T object, ConversionService conversionService); /** - * Is this object to map an instanceof this mappable type? - * @param object the object to test + * The names of the fields on the object that are eligible for mapping, including any nested fields. */ - boolean isInstance(Object object); + Set getFieldNames(T object); + + /** + * A map of the nested fields on the object that are nested relative to fieldName. + */ + Map getNestedFields(String fieldName, T object); } \ No newline at end of file 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 index 356810c9a55..16be45dde4e 100644 --- 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 @@ -15,6 +15,9 @@ */ package org.springframework.mapping.support; +import java.util.Map; + +import org.joda.time.DateTime; import org.springframework.core.convert.converter.Converter; import org.springframework.mapping.Mapper; @@ -107,6 +110,17 @@ public interface MapperBuilder { */ MapperBuilder addMapping(String[] fields, Mapper mapper); + /** + * Register a mapping that delegates to an assembler to convert multiple source field values to a single target field value. + * The source field names mapped begin with field.. + * For example, adding an assembler mapping for a field named dateTime would pass all nested dateTime.* fields to the assembler. + * Such fields might be year, month, day, etc. + * @param fields the name of the source field whose value will be assembled from multiple nested fields that begin with the same name + * @param converter the assembler that will perform value assembly from the field values + * @return this, for configuring additional field mapping options fluently + */ + MapperBuilder addAssemblerMapping(String field, Converter, ?> converter); + /** * Register a conditional mapping between a source field and a target field. * The source and target field names will be the same value. @@ -170,7 +184,7 @@ public interface MapperBuilder { * @param condition the boolean expression that determines if this mapping executes * @return this, for configuring additional field mapping options fluently */ - MapperBuilder addMapping(String[] fields, Mapper mapper, String condition); + MapperBuilder addConditionalMapping(String[] fields, Mapper mapper, String condition); /** * Register a Mapper that will be used to map between nested source and target fields of a specific sourceType/targetType pair. 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 c553fc8ac17..0679e91cd20 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 @@ -61,11 +61,7 @@ final class MappingConverter implements GenericConverter { } private boolean isCopyByReference(TypeDescriptor sourceType, TypeDescriptor targetType) { - if (BeanUtils.isSimpleValueType(targetType.getType())) { - return true; - } else { - return false; - } + return true; } private Object createTargetAndMap(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { 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 index b6847c08ad9..1adcc2ff5d5 100644 --- 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 @@ -15,9 +15,11 @@ */ package org.springframework.mapping.support; +import java.util.Map; + +import org.springframework.core.convert.converter.Converter; import org.springframework.core.style.StylerUtils; import org.springframework.expression.Expression; -import org.springframework.mapping.Mapper; /** * A mapping between several source fields and a target field. @@ -25,46 +27,51 @@ import org.springframework.mapping.Mapper; */ final class MultiFieldToFieldMapping implements SpelMapping { - private String[] fields; + private final String sourceField; + + private final Expression targetField; @SuppressWarnings("unchecked") - private final Mapper multiFieldMapper; + private final Converter targetFieldValueAssembler; - private Expression condition; - - public MultiFieldToFieldMapping(String[] fields, Mapper multiFieldMapper, Expression condition) { - this.fields = fields; - this.multiFieldMapper = multiFieldMapper; + private final Expression condition; + + public MultiFieldToFieldMapping(String sourceField, Expression targetField, Converter, ?> assembler, + Expression condition) { + this.sourceField = sourceField; + this.targetField = targetField; + this.targetFieldValueAssembler = assembler; this.condition = condition; } - public String[] getSourceFields() { - return fields; + public String getSourceField() { + return this.sourceField + ".*"; + } + + public String getTargetField() { + return this.targetField.getExpressionString(); } public boolean mapsField(String field) { - for (String f : this.fields) { - if (f.equals(field)) { - return true; - } - } - return false; + return field.startsWith(this.sourceField + "."); } @SuppressWarnings("unchecked") public void map(SpelMappingContext context) { if (!context.conditionHolds(this.condition)) { return; - } + } try { - this.multiFieldMapper.map(context.getSource(), context.getTarget()); + Map nestedFields = context.getSourceNestedFields(this.sourceField); + Object value = this.targetFieldValueAssembler.convert(nestedFields); + context.setTargetFieldValue(this.targetField, value); } catch (Exception e) { context.addMappingFailure(e); } } public int hashCode() { - return this.multiFieldMapper.hashCode(); + return getSourceField().hashCode() + getTargetField().hashCode(); } public boolean equals(Object o) { @@ -72,11 +79,11 @@ final class MultiFieldToFieldMapping implements SpelMapping { return false; } MultiFieldToFieldMapping m = (MultiFieldToFieldMapping) o; - return this.multiFieldMapper.equals(m.multiFieldMapper); + return getSourceField().equals(m.getSourceField()) && getTargetField().equals(m.getTargetField()); } public String toString() { - return "[MultiFieldToFieldMapping<" + StylerUtils.style(this.fields) + " -> " + this.multiFieldMapper + ">]"; + return "[MultiFieldToFieldMapping<" + StylerUtils.style(getSourceField()) + " -> " + getTargetField() + ">]"; } } \ 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 1c1d5813a7b..abb712caaf2 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,6 +17,7 @@ package org.springframework.mapping.support; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; @@ -24,7 +25,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.GenericTypeResolver; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterRegistry; -import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.ParseException; @@ -74,22 +74,25 @@ final class SpelMapper implements Mapper { public void setExcludedFields(String[] fields) { // TODO } - - public void addMapping(String sourceFieldExpression, String targetFieldExpression, Converter converter, + + public void addFieldToFieldMapping(String sourceField, String targetField, Converter converter, String condition) { - Expression sourceField = parseSourceField(sourceFieldExpression); - Expression targetField = parseTargetField(targetFieldExpression); - FieldToFieldMapping mapping = new FieldToFieldMapping(sourceField, targetField, converter, - parseCondition(condition)); - this.mappings.add(mapping); + this.mappings.add(new FieldToFieldMapping(parseSourceField(sourceField), parseTargetField(targetField), + converter, parseCondition(condition))); } - public void addMapping(String field, Mapper mapper, String condition) { + public void addFieldToMultiFieldMapping(String field, Mapper mapper, String condition) { this.mappings.add(new FieldToMultiFieldMapping(parseSourceField(field), mapper, parseCondition(condition))); } - public void addMapping(String[] fields, Mapper mapper, String condition) { - this.mappings.add(new MultiFieldToFieldMapping(fields, mapper, parseCondition(condition))); + public void addMultiFieldToFieldMapping(String[] fields, Mapper mapper, String condition) { + this.mappings.add(new FlexibleFieldMapping(fields, mapper, parseCondition(condition))); + } + + public void addMultiFieldToFieldMapping(String sourceField, String targetField, + Converter, ?> assembler, String condition) { + this.mappings.add(new MultiFieldToFieldMapping(sourceField, parseTargetField(targetField), assembler, + parseCondition(condition))); } public void addNestedMapper(Mapper nestedMapper, MappingTargetFactory targetFactory) { @@ -112,16 +115,17 @@ final class SpelMapper implements Mapper { Assert.notNull(target, "The target to map to cannot be null"); try { MappingContextHolder.push(source); - EvaluationContext sourceContext = getEvaluationContext(source); - EvaluationContext targetContext = getEvaluationContext(target); - SpelMappingContext context = new SpelMappingContext(sourceContext, targetContext); + MappableType sourceType = this.mappableTypeFactory.getMappableType(source); + MappableType targetType = this.mappableTypeFactory.getMappableType(target); + SpelMappingContext context = new SpelMappingContext(source, sourceType, target, targetType, + this.conversionService); for (SpelMapping mapping : this.mappings) { if (logger.isDebugEnabled()) { logger.debug(MappingContextHolder.getLevel() + mapping); } mapping.map(context); } - Set autoMappings = getAutoMappings(sourceContext, targetContext); + Set autoMappings = getAutoMappings(context); for (SpelMapping mapping : autoMappings) { if (logger.isDebugEnabled()) { logger.debug(MappingContextHolder.getLevel() + mapping + " (auto)"); @@ -171,33 +175,29 @@ final class SpelMapper implements Mapper { return GenericTypeResolver.resolveTypeArguments(mapper.getClass(), Mapper.class); } - private EvaluationContext getEvaluationContext(Object object) { - return mappableTypeFactory.getMappableType(object).getEvaluationContext(object, this.conversionService); - } - - private Set getAutoMappings(EvaluationContext sourceContext, EvaluationContext targetContext) { + private Set getAutoMappings(SpelMappingContext context) { if (this.autoMappingEnabled) { Set autoMappings = new LinkedHashSet(); - Set sourceFields = getMappableFields(sourceContext.getRootObject().getValue()); + Set sourceFields = context.getSourceFieldNames(); for (String field : sourceFields) { if (!explicitlyMapped(field)) { - Expression sourceExpression; - Expression targetExpression; + Expression sourceField; try { - sourceExpression = sourceExpressionParser.parseExpression(field); + sourceField = sourceExpressionParser.parseExpression(field); } catch (ParseException e) { - throw new IllegalArgumentException("The mapping source '" + field + throw new IllegalArgumentException("The source field '" + field + + "' is not a parseable value expression", e); + } + Expression targetField; + try { + targetField = targetExpressionParser.parseExpression(field); + } catch (ParseException e) { + throw new IllegalArgumentException("The target field '" + field + "' is not a parseable value expression", e); } try { - targetExpression = targetExpressionParser.parseExpression(field); - } catch (ParseException e) { - throw new IllegalArgumentException("The mapping target '" + field - + "' is not a parseable value expression", e); - } - try { - if (targetExpression.isWritable(targetContext)) { - autoMappings.add(new FieldToFieldMapping(sourceExpression, targetExpression, null, null)); + if (context.isTargetFieldWriteable(targetField)) { + autoMappings.add(new FieldToFieldMapping(sourceField, targetField, null, null)); } } catch (EvaluationException e) { @@ -209,11 +209,7 @@ final class SpelMapper implements Mapper { return Collections.emptySet(); } } - - private Set getMappableFields(Object object) { - return mappableTypeFactory.getMappableType(object).getFields(object); - } - + private boolean explicitlyMapped(String field) { for (SpelMapping mapping : this.mappings) { if (mapping.mapsField(field)) { 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 index cb4f5f84101..cef5ee80bf9 100644 --- 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 @@ -15,6 +15,8 @@ */ package org.springframework.mapping.support; +import java.util.Map; + import org.springframework.core.convert.converter.Converter; import org.springframework.mapping.Mapper; @@ -40,63 +42,68 @@ final class SpelMapperBuilder implements MapperBuilder { } public MapperBuilder addMapping(String field) { - this.mapper.addMapping(field, field, null, null); + this.mapper.addFieldToFieldMapping(field, field, null, null); return this; } public MapperBuilder addMapping(String field, Converter converter) { - this.mapper.addMapping(field, field, converter, null); + this.mapper.addFieldToFieldMapping(field, field, converter, null); return this; } public MapperBuilder addMapping(String field, Mapper mapper) { - this.mapper.addMapping(field, mapper, null); + this.mapper.addFieldToMultiFieldMapping(field, mapper, null); return this; } public MapperBuilder addMapping(String sourceField, String targetField) { - this.mapper.addMapping(sourceField, targetField, null, null); + this.mapper.addFieldToFieldMapping(sourceField, targetField, null, null); return this; } public MapperBuilder addMapping(String sourceField, String targetField, Converter converter) { - this.mapper.addMapping(sourceField, targetField, converter, null); + this.mapper.addFieldToFieldMapping(sourceField, targetField, converter, null); return this; } public MapperBuilder addMapping(String[] fields, Mapper mapper) { - this.mapper.addMapping(fields, mapper, null); + this.mapper.addMultiFieldToFieldMapping(fields, mapper, null); + return this; + } + + public MapperBuilder addAssemblerMapping(String field, Converter, ?> assembler) { + this.mapper.addMultiFieldToFieldMapping(field, field, assembler, null); return this; } public MapperBuilder addConditionalMapping(String field, String condition) { - this.mapper.addMapping(field, field, null, condition); + this.mapper.addFieldToFieldMapping(field, field, null, condition); return this; } public MapperBuilder addConditionalMapping(String field, Converter converter, String condition) { - this.mapper.addMapping(field, field, converter, condition); + this.mapper.addFieldToFieldMapping(field, field, converter, condition); return this; } public MapperBuilder addConditionalMapping(String field, Mapper mapper, String condition) { - this.mapper.addMapping(field, mapper, condition); + this.mapper.addFieldToMultiFieldMapping(field, mapper, condition); return this; } public MapperBuilder addConditionalMapping(String sourceField, String targetField, String condition) { - this.mapper.addMapping(sourceField, targetField, null, condition); + this.mapper.addFieldToFieldMapping(sourceField, targetField, null, condition); return this; } public MapperBuilder addConditionalMapping(String sourceField, String targetField, Converter converter, String condition) { - this.mapper.addMapping(sourceField, targetField, converter, condition); + this.mapper.addFieldToFieldMapping(sourceField, targetField, converter, condition); return this; } - public MapperBuilder addMapping(String[] fields, Mapper mapper, String condition) { - this.mapper.addMapping(fields, mapper, condition); + public MapperBuilder addConditionalMapping(String[] fields, Mapper mapper, String condition) { + this.mapper.addMultiFieldToFieldMapping(fields, mapper, condition); return this; } 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 index be4a5614a44..0053587e415 100644 --- 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 @@ -17,7 +17,10 @@ package org.springframework.mapping.support; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.core.convert.ConversionService; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.mapping.MappingException; @@ -25,21 +28,25 @@ import org.springframework.mapping.MappingFailure; final class SpelMappingContext { + private final MappableType sourceType; + 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 SpelMappingContext(Object source, MappableType sourceType, Object target, MappableType targetType, + ConversionService conversionService) { + this.sourceType = sourceType; + this.sourceEvaluationContext = sourceType.getEvaluationContext(source, conversionService); + this.targetEvaluationContext = targetType.getEvaluationContext(target, conversionService); } public Object getSource() { return this.sourceEvaluationContext.getRootObject().getValue(); } - + public Object getTarget() { return this.targetEvaluationContext.getRootObject().getValue(); } @@ -55,6 +62,14 @@ final class SpelMappingContext { return sourceField.getValue(this.sourceEvaluationContext); } + public Set getSourceFieldNames() { + return this.sourceType.getFieldNames(getSource()); + } + + public Map getSourceNestedFields(String sourceFieldName) { + return this.sourceType.getNestedFields(sourceFieldName, getSource()); + } + public void setTargetFieldValue(Expression targetField, Object value) { targetField.setValue(this.targetEvaluationContext, value); } @@ -69,4 +84,8 @@ final class SpelMappingContext { } } + public boolean isTargetFieldWriteable(Expression targetField) { + return targetField.isWritable(this.targetEvaluationContext); + } + } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/AnnotationFormatterFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/AnnotationFormatterFactory.java index 9b74add6302..c27f33fcbf7 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/AnnotationFormatterFactory.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/AnnotationFormatterFactory.java @@ -13,31 +13,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.ui.format; import java.lang.annotation.Annotation; +import java.util.Set; /** - * A factory that creates {@link Formatter formatters} to format property values on properties - * annotated with a particular format {@link Annotation}. + * A factory that creates formatters to format values of fields annotated with a particular format {@link Annotation}. * - *

For example, a CurrencyAnnotationFormatterFactory might create a Formatter - * that formats a BigDecimal value set on a property annotated with @CurrencyFormat. + *

For example, a DateTimeFormatAnnotationFormatterFactory might create a formatter + * that formats a Date objects set on properties annotated with @DateFormat. * * @author Keith Donald * @since 3.0 - * @param the type of Annotation this factory uses to create Formatter instances - * @param the type of object that the factory's Formatters are dealing with + * @param the type of Annotation that should trigger property formatting */ -public interface AnnotationFormatterFactory { - - /** - * Get the Formatter that will format the value of the property annotated with the provided annotation. - * The annotation instance can contain properties that may be used to configure the Formatter that is returned. - * @param annotation the annotation instance - * @return the Formatter to use to format values of properties annotated with the annotation. - */ - Formatter getFormatter(A annotation); +public interface AnnotationFormatterFactory { + /** + * The types of fields that may be annotated with the <A> annotation. + */ + Set> getFieldTypes(); + + /** + * Get the Printer to print the value of a property of fieldType annotated with annotation. + * @param annotation the annotation instance + * @param fieldType the type of property being annotated + * @return the printer + */ + Printer getPrinter(A annotation, Class fieldType); + + /** + * Get the Parser to parse the printed value of a property of fieldType annotated with annotation. + * @param annotation the annotation instance + * @param fieldType the type of field being annotated + * @return the parser + */ + Parser getParser(A annotation, Class fieldType); + } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java index 7c27339618e..af50378137b 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java @@ -16,35 +16,14 @@ package org.springframework.ui.format; -import java.text.ParseException; -import java.util.Locale; - /** - * Formats objects of type T for display. + * Formats objects of type T. + * A Formatter is both a Printer and a Parser for an object type. * * @author Keith Donald * @since 3.0 - * @param the type of object this formatter can format + * @param the type of object this Formatter formats */ -public interface Formatter { - - /** - * Format the object of type T for display. - * @param object the object to format - * @param locale the user's locale - * @return the formatted display string - */ - String format(T object, Locale locale); - - /** - * Parse an object from its formatted representation. - * @param formatted a formatted representation - * @param locale the user's locale - * @return the parsed object - * @throws ParseException when a parse exception occurs - * @throws RuntimeException when thrown by coercion methods that are - * - */ - T parse(String formatted, Locale locale) throws ParseException; +public interface Formatter extends Printer, Parser { } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/FormatterRegistry.java b/org.springframework.context/src/main/java/org/springframework/ui/format/FormatterRegistry.java index 91d1d7c324a..38864d1de91 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/FormatterRegistry.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/FormatterRegistry.java @@ -13,15 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.ui.format; -import java.lang.annotation.Annotation; - -import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConverterRegistry; /** - * A shared registry of Formatters. + * A registry of field formatting logic. * * @author Keith Donald * @since 3.0 @@ -29,46 +26,43 @@ import org.springframework.core.convert.TypeDescriptor; public interface FormatterRegistry { /** - * Adds a Formatter to this registry indexed by type. - *

Use this add method when type differs from <T>. - * Calling getFormatter(type) returns a decorator that wraps - * the targetFormatter instance. - *

On format, the decorator first coerces the instance of type to <T>, - * then delegates to targetFormatter to format the value. - *

On parse, the decorator first delegates to the formatter to parse a <T>, - * then coerces the parsed value to type. - * @param type the object type - * @param targetFormatter the target formatter + * Adds a Formatter to format fields of a specific type. + * The Formatter will delegate to the specified printer for printing and parser for parsing. + *

+ * On print, if the Printer's type T is declared and fieldType is not assignable to T, + * a coersion to T will be attempted before delegating to printer to print a field value. + * On parse, if the object returned by the Parser is not assignable to the runtime field type, + * a coersion to the field type will be attempted before returning the parsed field value. + * @param fieldType the field type to format + * @param formatter the formatter to add */ - void addFormatterByType(Class type, Formatter targetFormatter); + void addFormatterForFieldType(Class fieldType, Printer printer, Parser parser); /** - * Adds a Formatter to this registry indexed by <T>. - *

Calling getFormatter(<T>.class) returns formatter. - * @param formatter the formatter + * Adds a Formatter to format fields of a specific type. + *

+ * On print, if the Formatter's type T is declared and fieldType is not assignable to T, + * a coersion to T will be attempted before delegating to formatter to print a field value. + * On parse, if the parsed object returned by formatter is not assignable to the runtime field type, + * a coersion to the field type will be attempted before returning the parsed field value. + * @param fieldType the field type to format + * @param formatter the formatter to add */ - void addFormatterByType(Formatter formatter); + void addFormatterForFieldType(Class fieldType, Formatter formatter); /** - * Adds a Formatter to this registry indexed by the given annotation type. - *

Calling getFormatter(...) on a field or accessor method - * with the given annotation returns formatter. - * @param formatter the formatter + * Adds a Formatter to format fields annotated with a specific format annotation. + * @param annotationFormatterFactory the annotation formatter factory to add */ - void addFormatterByAnnotation(Class annotationType, Formatter formatter); - + void addFormatterForFieldAnnotation(AnnotationFormatterFactory annotationFormatterFactory); + /** - * Adds a AnnotationFormatterFactory that returns the Formatter for properties annotated with a specific annotation. - *

Calling getFormatter(...) on a field or accessor method - * with the given annotation returns formatter. - * @param factory the annotation formatter factory + * Returns the registry of Converters that coerse field values to types required by Formatters. + * Allows clients to register their own custom converters. + * For example, a date/time formatting configuration might expect a java.util.Date field value to be converted to a Long for formatting. + * Registering a simpler DateToLongConverter allievates the need to register multiple formatters for closely related types. + * @return the converter registry, allowing new Converters to be registered */ - void addFormatterByAnnotation(AnnotationFormatterFactory factory); - - /** - * Get the Formatter for the type descriptor. - * @return the Formatter, or null if no suitable one is registered - */ - Formatter getFormatter(TypeDescriptor type); + ConverterRegistry getConverterRegistry(); } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/FormattingService.java b/org.springframework.context/src/main/java/org/springframework/ui/format/FormattingService.java new file mode 100644 index 00000000000..91f58c1d3d9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/FormattingService.java @@ -0,0 +1,50 @@ +/* + * 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.ui.format; + +import java.text.ParseException; +import java.util.Locale; + +import org.springframework.core.convert.TypeDescriptor; + +/** + * A service interface for formatting localized field values. + * This is the entry point into the ui.format system. + * + * @author Keith Donald + * @since 3.0 + */ +public interface FormattingService { + + /** + * Print the field value for display in the locale. + * @param fieldValue the field value + * @param fieldType the field type + * @param locale the user's locale + * @return the printed string + */ + String print(Object fieldValue, TypeDescriptor fieldType, Locale locale); + + /** + * Parse the the value submitted by the user. + * @param submittedValue the submitted field value + * @param fieldType the field type + * @param locale the user's locale + * @return the parsed field value + */ + Object parse(String submittedValue, TypeDescriptor fieldType, Locale locale) throws ParseException; + +} diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/DateToCalendarConverter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/Parser.java similarity index 50% rename from org.springframework.core/src/main/java/org/springframework/core/convert/support/DateToCalendarConverter.java rename to org.springframework.context/src/main/java/org/springframework/ui/format/Parser.java index c0538910a03..eccf7a5f006 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/DateToCalendarConverter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/Parser.java @@ -13,23 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.core.convert.support; -import java.util.Calendar; -import java.util.Date; +package org.springframework.ui.format; -import org.springframework.core.convert.converter.Converter; +import java.text.ParseException; +import java.util.Locale; /** - * Converts from a java.util.Date to a java.util.Calendar. + * Parses objects of type T from their printed representations. + * * @author Keith Donald + * @since 3.0 + * @param the type of object this Parser parses */ -final class DateToCalendarConverter implements Converter { +public interface Parser { - public Calendar convert(Date source) { - Calendar cal = Calendar.getInstance(); - cal.setTime(source); - return cal; - } + /** + * Parse an object from its printed representation. + * @param printed a printed representation + * @param locale the current user locale + * @return the parsed object + * @throws ParseException when a parse exception occurs in a java.text parsing library + * @throws RuntimeException when a parse exception occurs + */ + T parse(String printed, Locale locale) throws ParseException; } diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/CalendarToDateConverter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/Printer.java similarity index 61% rename from org.springframework.core/src/main/java/org/springframework/core/convert/support/CalendarToDateConverter.java rename to org.springframework.context/src/main/java/org/springframework/ui/format/Printer.java index bb41a89ad13..f7c025a1d8e 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/CalendarToDateConverter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/Printer.java @@ -13,21 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.core.convert.support; -import java.util.Calendar; -import java.util.Date; +package org.springframework.ui.format; -import org.springframework.core.convert.converter.Converter; +import java.util.Locale; /** - * Converts from a java.util.Calendar to a java.util.Date. + * Prints objects of type T for display. + * * @author Keith Donald + * @since 3.0 + * @param the type of object this Printer prints */ -final class CalendarToDateConverter implements Converter { - - public Date convert(Calendar source) { - return source.getTime(); - } - +public interface Printer { + + /** + * Print the object of type T for display. + * @param object the object to print + * @param locale the current user locale + * @return the printed string + */ + String print(T object, Locale locale); + } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/date/DateFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/date/DateFormatter.java index e958e35e599..066600892e7 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/date/DateFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/date/DateFormatter.java @@ -97,17 +97,11 @@ public class DateFormatter implements Formatter { } - public String format(Date date, Locale locale) { - if (date == null) { - return ""; - } + public String print(Date date, Locale locale) { return getDateFormat(locale).format(date); } public Date parse(String formatted, Locale locale) throws ParseException { - if (formatted.length() == 0) { - return null; - } return getDateFormat(locale).parse(formatted); } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/AbstractDateTimeAnnotationFormatterFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/AbstractDateTimeAnnotationFormatterFactory.java new file mode 100644 index 00000000000..9d705b700ef --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/AbstractDateTimeAnnotationFormatterFactory.java @@ -0,0 +1,89 @@ +/* + * 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.ui.format.jodatime; + +import java.lang.annotation.Annotation; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import org.joda.time.DateTime; +import org.joda.time.ReadableInstant; +import org.joda.time.ReadablePartial; +import org.joda.time.format.DateTimeFormatter; +import org.springframework.ui.format.AnnotationFormatterFactory; +import org.springframework.ui.format.Parser; +import org.springframework.ui.format.Printer; + +/** + * Base class for annotation-based Joda DateTime formatters. + * Can format any Joda {@link ReadableInstant} or {@link ReadablePartial} property, as well as standard {@link Date}, {@link Calendar}, and Long properties. + * @author Keith Donald + * @param the format annotation parameter type to be declared by subclasses + */ +abstract class AbstractDateTimeAnnotationFormatterFactory implements AnnotationFormatterFactory { + + private final Set> propertyTypes; + + public AbstractDateTimeAnnotationFormatterFactory() { + this.propertyTypes = Collections.unmodifiableSet(createPropertyTypes()); + } + + public Set> getFieldTypes() { + return propertyTypes; + } + + public Printer getPrinter(A annotation, Class propertyType) { + DateTimeFormatter formatter = configureDateTimeFormatterFrom(annotation); + if (ReadableInstant.class.isAssignableFrom(propertyType)) { + return new ReadableInstantPrinter(formatter); + } else if (ReadablePartial.class.equals(propertyType)) { + return new ReadablePartialPrinter(formatter); + } else if (Calendar.class.isAssignableFrom(propertyType)) { + // assumes Calendar->ReadableInstant converter is registered + return new ReadableInstantPrinter(formatter); + } else { + // assumes Date->Long converter is registered + return new MillisecondInstantPrinter(formatter); + } + } + + public Parser getParser(A annotation, Class propertyType) { + return new DateTimeParser(configureDateTimeFormatterFrom(annotation)); + } + + /** + * Hook method subclasses should override to configure the Joda {@link DateTimeFormatter} from the annotation values. + * @param annotation the annotation containing formatter configuration rules + * @return the configured date time formatter + */ + protected abstract DateTimeFormatter configureDateTimeFormatterFrom(A annotation); + + // internal helpers + + private Set> createPropertyTypes() { + Set> propertyTypes = new HashSet>(); + propertyTypes.add(ReadableInstant.class); + propertyTypes.add(ReadablePartial.class); + propertyTypes.add(Date.class); + propertyTypes.add(Calendar.class); + propertyTypes.add(Long.class); + return propertyTypes; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormat.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormat.java new file mode 100644 index 00000000000..d6a065c18e9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormat.java @@ -0,0 +1,80 @@ +/* + * 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.ui.format.jodatime; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a property should be formatted as a date time. + * @author Keith Donald + */ +@Target( { ElementType.METHOD, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface DateTimeFormat { + + /** + * The style to use for formatting the date portion of the property. + * Defaults to {@link FormatStyle#NONE}. + */ + FormatStyle dateStyle() default FormatStyle.NONE; + + /** + * The style to use for formatting the time portion of the property. + * Defaults to {@link FormatStyle#NONE}. + */ + FormatStyle timeStyle() default FormatStyle.NONE; + + /** + * A pattern String that defines a custom format for the property. + * Use this method or the *Style methods, not both. + */ + String pattern() default ""; + + /** + * Supported DateTimeFormat styles. + */ + public enum FormatStyle { + SHORT { + public String toString() { + return "S"; + } + }, + MEDIUM { + public String toString() { + return "M"; + } + }, + LONG { + public String toString() { + return "L"; + } + }, + FULL { + public String toString() { + return "F"; + } + }, + NONE { + public String toString() { + return "-"; + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormatAnnotationFormatterFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormatAnnotationFormatterFactory.java new file mode 100644 index 00000000000..ed2c515bbcf --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormatAnnotationFormatterFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ui.format.jodatime; + +import org.joda.time.format.DateTimeFormatter; +import org.springframework.ui.format.jodatime.DateTimeFormat.FormatStyle; + +/** + * Formats properties annotated with the {@link DateTimeFormat} annotation. + * + * @author Keith Donald + * @since 3.0 + * @see DateTimeFormat + */ +public final class DateTimeFormatAnnotationFormatterFactory extends AbstractDateTimeAnnotationFormatterFactory { + + protected DateTimeFormatter configureDateTimeFormatterFrom(DateTimeFormat annotation) { + String pattern = annotation.pattern(); + if (!pattern.isEmpty()) { + return forPattern(pattern); + } else { + FormatStyle dateStyle = annotation.dateStyle(); + FormatStyle timeStyle = annotation.timeStyle(); + return forDateTimeStyle(dateStyle, timeStyle); + } + } + + private DateTimeFormatter forDateTimeStyle(FormatStyle dateStyle, FormatStyle timeStyle) { + return org.joda.time.format.DateTimeFormat.forStyle(dateStyle.toString() + timeStyle.toString()); + } + + private DateTimeFormatter forPattern(String pattern) { + return org.joda.time.format.DateTimeFormat.forPattern(pattern); + } + +} \ No newline at end of file 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 deleted file mode 100644 index 4700cec1642..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeFormatter.java +++ /dev/null @@ -1,57 +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.ui.format.jodatime; - -import java.text.ParseException; -import java.util.Locale; - -import org.joda.time.DateTime; -import org.joda.time.format.DateTimeFormat; -import org.springframework.ui.format.Formatter; - -/** - * {@link Formatter} implementation to handle JodaTime {@link DateTime} instances. - * @author Oliver Gierke - */ -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#forPattern(String) - */ - public DateTimeFormatter(String pattern) { - this.formatter = DateTimeFormat.forPattern(pattern); - } - - /** - * 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; - } - - public String format(DateTime object, Locale locale) { - return null == object ? "" : object.toString(formatter); - } - - public DateTime parse(String formatted, Locale locale) throws ParseException { - return formatter.withLocale(locale).parseDateTime(formatted); - } -} diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/jodatime/DateTimeFormatterTests.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeParser.java similarity index 55% rename from org.springframework.context/src/test/java/org/springframework/ui/format/jodatime/DateTimeFormatterTests.java rename to org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeParser.java index 0973887cf4d..9d41cb5a097 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/format/jodatime/DateTimeFormatterTests.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/DateTimeParser.java @@ -13,35 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.ui.format.jodatime; -import static org.junit.Assert.assertEquals; - import java.text.ParseException; import java.util.Locale; import org.joda.time.DateTime; -import org.joda.time.format.DateTimeFormat; -import org.junit.Test; +import org.joda.time.format.DateTimeFormatter; +import org.springframework.ui.format.Parser; /** + * Parses Joda Time {@link DateTime} instances using a {@link DateTimeFormatter}. * @author Keith Donald */ -public class DateTimeFormatterTests { +public final class DateTimeParser implements Parser { - private DateTimeFormatter formatter = new DateTimeFormatter("yyyy-MM-dd"); - - @Test - public void formatValue() { - DateTime dateTime = DateTimeFormat.forPattern("yyyy-MM-dd").parseDateTime("2009-06-01"); - assertEquals("2009-06-01", formatter.format(dateTime, Locale.US)); - } - - @Test - public void parseValue() throws ParseException { - DateTime dateTime = DateTimeFormat.forPattern("yyyy-MM-dd").parseDateTime("2009-06-01"); - assertEquals(dateTime, formatter.parse("2009-06-01", Locale.US)); + private final DateTimeFormatter formatter; + + /** + * Creates a new DateTimeParser. + * @param formatter the Joda DateTimeFormatter instance + */ + public DateTimeParser(DateTimeFormatter formatter) { + this.formatter = formatter; } + public DateTime parse(String printed, Locale locale) throws ParseException { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).parseDateTime(printed); + } } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/Formatted.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ISODateTimeFormat.java similarity index 70% rename from org.springframework.context/src/main/java/org/springframework/ui/format/Formatted.java rename to org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ISODateTimeFormat.java index 41906262094..46e34c3d7b1 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/Formatted.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ISODateTimeFormat.java @@ -13,29 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.springframework.ui.format.jodatime; -package org.springframework.ui.format; - -import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * A type that can be formatted as a String for display in a user interface. - * + * Indicates a property should be formatted as a ISO date time. * @author Keith Donald - * @since 3.0 */ -@Target({ElementType.TYPE}) +@Target( { ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface Formatted { +public @interface ISODateTimeFormat { - /** - * The Formatter that handles the formatting for the annotated element. - */ - Class value(); + FormatStyle value(); + public enum FormatStyle { + DATE, TIME, DATE_TIME + } + } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ISODateTimeFormatAnnotationFormatterFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ISODateTimeFormatAnnotationFormatterFactory.java new file mode 100644 index 00000000000..ca5efb019e0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ISODateTimeFormatAnnotationFormatterFactory.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.ui.format.jodatime; + +import org.joda.time.format.DateTimeFormatter; +import org.springframework.ui.format.jodatime.ISODateTimeFormat.FormatStyle; + +/** + * Formats properties annotated with the {@link ISODateTimeFormat} annotation. + * + * @author Keith Donald + * @since 3.0 + * @see ISODateTimeFormat + */ +public final class ISODateTimeFormatAnnotationFormatterFactory extends AbstractDateTimeAnnotationFormatterFactory { + + protected DateTimeFormatter configureDateTimeFormatterFrom(ISODateTimeFormat annotation) { + FormatStyle style = annotation.value(); + if (style == FormatStyle.DATE) { + return org.joda.time.format.ISODateTimeFormat.date(); + } else if (style == FormatStyle.TIME) { + return org.joda.time.format.ISODateTimeFormat.time(); + } else { + return org.joda.time.format.ISODateTimeFormat.dateTime(); + } + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeContext.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeContext.java new file mode 100644 index 00000000000..f0f510dd09d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeContext.java @@ -0,0 +1,79 @@ +/* + * 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.ui.format.jodatime; + +import org.joda.time.Chronology; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormatter; + +/** + * A context that holds user-specific Joda Time settings such as the user's Chronology (calendar system) and time zone. + * A null property value indicate the user has not specified a setting. + * @author Keith Donald + * @see JodaTimeContextHolder + */ +public class JodaTimeContext { + + private Chronology chronology; + + private DateTimeZone timeZone; + + /** + * The user's chronology (calendar system). + * Null if not specified. + */ + public Chronology getChronology() { + return chronology; + } + + /** + * Sets the user's chronology. + */ + public void setChronology(Chronology chronology) { + this.chronology = chronology; + } + + /** + * The user's timezone. + * Null if not specified. + */ + public DateTimeZone getTimeZone() { + return timeZone; + } + + /** + * Sets the user's timezone. + */ + public void setTimeZone(DateTimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Gets the Formatter with the this context's settings applied to the base formatter. + * @param formatter the base formatter that establishes default formatting rules, generally context independent + * @return the context DateTimeFormatter + */ + public DateTimeFormatter getFormatter(DateTimeFormatter formatter) { + if (this.chronology != null) { + formatter = formatter.withChronology(this.chronology); + } + if (this.timeZone != null) { + formatter = formatter.withZone(this.timeZone); + } + return formatter; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeContextHolder.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeContextHolder.java new file mode 100644 index 00000000000..98e2199dcb0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeContextHolder.java @@ -0,0 +1,63 @@ +/* + * 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.ui.format.jodatime; + +import java.util.Locale; + +import org.joda.time.format.DateTimeFormatter; +import org.springframework.core.NamedInheritableThreadLocal; + +/** + * A holder for a thread-local user {@link JodaTimeContext}. + * @author Keith Donald + */ +public final class JodaTimeContextHolder { + + private static final ThreadLocal jodaTimeContextHolder = new NamedInheritableThreadLocal( + "Joda Time Context"); + + /** + * Return the JodaTimeContext associated with the current thread, if any. + * @return the current JodaTimeContext, or null if none + */ + public static JodaTimeContext getJodaTimeContext() { + return jodaTimeContextHolder.get(); + } + + /** + * Associate the given JodaTimeContext with the current thread. + * @param context the current JodaTimeContext, or null to clear + * the thread-bound context + */ + public static void setLocaleContext(JodaTimeContext context) { + jodaTimeContextHolder.set(context); + } + + /** + * Gets the Formatter with the user-specific settings applied to thefrom the base formatter. + * @param formatter the base formatter that establishes default formatting rules, generally user independent + * @param locale the current user locale (may be null if not known) + * @return the user's DateTimeFormatter + */ + public static DateTimeFormatter getFormatter(DateTimeFormatter formatter, Locale locale) { + if (locale != null) { + formatter = formatter.withLocale(locale); + } + JodaTimeContext context = getJodaTimeContext(); + return context != null ? context.getFormatter(formatter) : formatter; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeConverters.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeConverters.java new file mode 100644 index 00000000000..538da9b710b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeConverters.java @@ -0,0 +1,113 @@ +/* + * 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.ui.format.jodatime; + +import java.util.Calendar; +import java.util.Date; + +import org.joda.time.DateMidnight; +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.LocalTime; +import org.joda.time.ReadableInstant; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterRegistry; + +/** + * Installs lower-level type converters required to integrate Joda Time support into Spring's formatting and property binding systems. + * @author Keith Donald + */ +final class JodaTimeConverters { + + private JodaTimeConverters() { + + } + + /** + * Installs the converters into the converter registry. + * @param registry the converter registry + */ + public static void registerConverters(ConverterRegistry registry) { + registry.addConverter(new DateTimeToLocalDateConverter()); + registry.addConverter(new DateTimeToLocalTimeConverter()); + registry.addConverter(new DateTimeToLocalDateTimeConverter()); + registry.addConverter(new DateTimeToDateMidnightConverter()); + registry.addConverter(new DateTimeToDateConverter()); + registry.addConverter(new DateTimeToCalendarConverter()); + registry.addConverter(new DateToLongConverter()); + registry.addConverter(new CalendarToDateTimeConverter()); + } + + // internal helpers + + // used when binding a parsed DateTime to a LocalDate property + private static class DateTimeToLocalDateConverter implements Converter { + public LocalDate convert(DateTime source) { + return source.toLocalDate(); + } + } + + // used when binding a parsed DateTime to a LocalTime property + private static class DateTimeToLocalTimeConverter implements Converter { + public LocalTime convert(DateTime source) { + return source.toLocalTime(); + } + } + + // used when binding a parsed DateTime to a LocalDateTime property + private static class DateTimeToLocalDateTimeConverter implements Converter { + public LocalDateTime convert(DateTime source) { + return source.toLocalDateTime(); + } + } + + // used when binding a parsed DateTime to a DateMidnight property + private static class DateTimeToDateMidnightConverter implements Converter { + public DateMidnight convert(DateTime source) { + return source.toDateMidnight(); + } + } + + // used when binding a parsed DateTime to a java.util.Date property + private static class DateTimeToDateConverter implements Converter { + public Date convert(DateTime source) { + return source.toDate(); + } + } + + // used when binding a parsed DateTime to a java.util.Calendar property + private static class DateTimeToCalendarConverter implements Converter { + public Calendar convert(DateTime source) { + return source.toGregorianCalendar(); + } + } + + // used when formatting a java.util.Date property with a MillisecondInstantPrinter + private static class DateToLongConverter implements Converter { + public Long convert(Date source) { + return source.getTime(); + } + } + + // used when formatting a java.util.Calendar property with a ReadableInstantPrinter + private static class CalendarToDateTimeConverter implements Converter { + public ReadableInstant convert(Calendar source) { + return new DateTime(source); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeFormattingConfigurer.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeFormattingConfigurer.java new file mode 100644 index 00000000000..36fdd2af158 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/JodaTimeFormattingConfigurer.java @@ -0,0 +1,154 @@ +/* + * 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.ui.format.jodatime; + +import java.util.Calendar; +import java.util.Date; + +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.LocalTime; +import org.joda.time.ReadableInstant; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.springframework.ui.format.FormatterRegistry; +import org.springframework.ui.format.Parser; +import org.springframework.ui.format.Printer; + +/** + * Configures Joda Time's Formatting system for use with Spring. + * @author Keith Donald + */ +public class JodaTimeFormattingConfigurer { + + private FormatterRegistry formatterRegistry; + + private String dateStyle; + + private String timeStyle; + + private String dateTimeStyle; + + private boolean useISOFormat; + + /** + * Creates a new JodaTimeFormattingConfigurer that installs into the provided FormatterRegistry. + * Call {@link #registerJodaTimeFormatting()} to install. + * @param formatterRegistry the registry to register Joda Time formatters with + */ + public JodaTimeFormattingConfigurer(FormatterRegistry formatterRegistry) { + this.formatterRegistry = formatterRegistry; + } + + /** + * Set the default format style of Joda {@link LocalDate} objects. + * Default is {@link DateTimeFormat#shortDate()}. + * @param dateStyle the date format style + */ + public void setDateStyle(String dateStyle) { + this.dateStyle = dateStyle; + } + + /** + * Set the default format style of Joda {@link LocalTime} objects. + * Default is {@link DateTimeFormat#shortTime()}. + * @param timeStyle the time format style + */ + public void setTimeStyle(String timeStyle) { + this.timeStyle = timeStyle; + } + + /** + * Set the default format style of Joda {@link LocalDateTime} and {@link DateTime} objects, as well as JDK {@link Date} and {@link Calendar} objects. + * Default is {@link DateTimeFormat#shortDateTime()}. + * @param dateTimeStyle the date time format style + */ + public void setDateTimeStyle(String dateTimeStyle) { + this.dateTimeStyle = dateTimeStyle; + } + + /** + * Set whether standard ISO formatting should be applied to all Date/Time types. + * Default is false (no). + * If set to true, the dateStyle, timeStyle, and dateTimeStyle properties are ignored. + * @param useISOFormat true to enable ISO formatting + */ + public void setUseISOFormat(boolean useISOFormat) { + this.useISOFormat = useISOFormat; + } + + /** + * Install Joda Time formatters given the current configuration of this {@link JodaTimeFormattingConfigurer}. + */ + public void registerJodaTimeFormatting() { + JodaTimeConverters.registerConverters(this.formatterRegistry.getConverterRegistry()); + + DateTimeFormatter jodaDateFormatter = getJodaDateFormatter(); + this.formatterRegistry.addFormatterForFieldType(LocalDate.class, new ReadablePartialPrinter(jodaDateFormatter), new DateTimeParser(jodaDateFormatter)); + + DateTimeFormatter jodaTimeFormatter = getJodaTimeFormatter(); + this.formatterRegistry.addFormatterForFieldType(LocalTime.class, new ReadablePartialPrinter(jodaTimeFormatter), new DateTimeParser(jodaTimeFormatter)); + + DateTimeFormatter jodaDateTimeFormatter = getJodaDateTimeFormatter(); + Parser dateTimeParser = new DateTimeParser(jodaDateTimeFormatter); + this.formatterRegistry.addFormatterForFieldType(LocalDateTime.class, new ReadablePartialPrinter(jodaDateTimeFormatter), dateTimeParser); + + Printer readableInstantPrinter = new ReadableInstantPrinter(jodaDateTimeFormatter); + this.formatterRegistry.addFormatterForFieldType(ReadableInstant.class, readableInstantPrinter, dateTimeParser); + this.formatterRegistry.addFormatterForFieldType(Calendar.class, readableInstantPrinter, dateTimeParser); + this.formatterRegistry.addFormatterForFieldType(Calendar.class, new MillisecondInstantPrinter(jodaDateTimeFormatter), dateTimeParser); + + this.formatterRegistry.addFormatterForFieldAnnotation(new DateTimeFormatAnnotationFormatterFactory()); + } + + // internal helpers + + private DateTimeFormatter getJodaDateFormatter() { + if (this.useISOFormat) { + return ISODateTimeFormat.date(); + } + if (this.dateStyle != null) { + return DateTimeFormat.forStyle(this.dateStyle + "-"); + } else { + return DateTimeFormat.shortDate(); + } + } + + private DateTimeFormatter getJodaTimeFormatter() { + if (this.useISOFormat) { + return ISODateTimeFormat.time(); + } + if (this.timeStyle != null) { + return DateTimeFormat.forStyle("-" + this.timeStyle); + } else { + return DateTimeFormat.shortTime(); + } + } + + private DateTimeFormatter getJodaDateTimeFormatter() { + if (this.useISOFormat) { + return ISODateTimeFormat.dateTime(); + } + if (this.dateTimeStyle != null) { + return DateTimeFormat.forStyle(this.dateTimeStyle); + } else { + return DateTimeFormat.shortDateTime(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/MillisecondInstantPrinter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/MillisecondInstantPrinter.java new file mode 100644 index 00000000000..8467cae7857 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/MillisecondInstantPrinter.java @@ -0,0 +1,42 @@ +/* + * 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.ui.format.jodatime; + +import java.util.Locale; + +import org.joda.time.format.DateTimeFormatter; +import org.springframework.ui.format.Printer; + +/** + * Prints Long instances using a {@link DateTimeFormatter}. + * @author Keith Donald + */ +public final class MillisecondInstantPrinter implements Printer { + + private final DateTimeFormatter formatter; + + /** + * Creates a new ReadableInstantPrinter. + * @param formatter the Joda DateTimeFormatter instance + */ + public MillisecondInstantPrinter(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + public String print(Long instant, Locale locale) { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).print(instant); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ReadableInstantPrinter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ReadableInstantPrinter.java new file mode 100644 index 00000000000..81a9617299a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ReadableInstantPrinter.java @@ -0,0 +1,43 @@ +/* + * 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.ui.format.jodatime; + +import java.util.Locale; + +import org.joda.time.ReadableInstant; +import org.joda.time.format.DateTimeFormatter; +import org.springframework.ui.format.Printer; + +/** + * Prints Joda Time {@link ReadableInstant} instances using a {@link DateTimeFormatter}. + * @author Keith Donald + */ +public final class ReadableInstantPrinter implements Printer { + + private final DateTimeFormatter formatter; + + /** + * Creates a new ReadableInstantPrinter. + * @param formatter the Joda DateTimeFormatter instance + */ + public ReadableInstantPrinter(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + public String print(ReadableInstant instant, Locale locale) { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).print(instant); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ReadablePartialPrinter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ReadablePartialPrinter.java new file mode 100644 index 00000000000..a10f00d1803 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/ReadablePartialPrinter.java @@ -0,0 +1,43 @@ +/* + * 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.ui.format.jodatime; + +import java.util.Locale; + +import org.joda.time.ReadablePartial; +import org.joda.time.format.DateTimeFormatter; +import org.springframework.ui.format.Printer; + +/** + * Prints Joda Time {@link ReadablePartial} instances using a {@link DateTimeFormatter}. + * @author Keith Donald + */ +public final class ReadablePartialPrinter implements Printer { + + private final DateTimeFormatter formatter; + + /** + * Creates a new ReadableInstantPrinter. + * @param formatter the Joda DateTimeFormatter instance + */ + public ReadablePartialPrinter(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + public String print(ReadablePartial partial, Locale locale) { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).print(partial); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/package-info.java b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/package-info.java new file mode 100644 index 00000000000..fe97c983c0a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/jodatime/package-info.java @@ -0,0 +1,4 @@ +/** + * Integration with the Joda Time for formatting Joda types as well as standard JDK Date types. + */ +package org.springframework.ui.format.jodatime; diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/AbstractNumberFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/AbstractNumberFormatter.java index 5dc4b1c0c85..e6781880159 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/AbstractNumberFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/AbstractNumberFormatter.java @@ -46,18 +46,11 @@ public abstract class AbstractNumberFormatter implements Formatter { } - public String format(Number integer, Locale locale) { - if (integer == null) { - return ""; - } - NumberFormat format = getNumberFormat(locale); - return format.format(integer); + public String print(Number integer, Locale locale) { + return getNumberFormat(locale).format(integer); } public Number parse(String formatted, Locale locale) throws ParseException { - if (formatted.length() == 0) { - return null; - } NumberFormat format = getNumberFormat(locale); ParsePosition position = new ParsePosition(0); Number number = format.parse(formatted, position); diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/package-info.java b/org.springframework.context/src/main/java/org/springframework/ui/format/package-info.java index f0dd27efba4..d9582084691 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/package-info.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/package-info.java @@ -1,4 +1,4 @@ /** - * A SPI for defining Formatters to format field model values for display in a UI. + * An API for defining Formatters to format field model values for display in a UI. */ package org.springframework.ui.format; diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/support/FormattingConversionServiceAdapter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/support/FormattingConversionServiceAdapter.java index 81b5a9b855e..a3360cb4cc7 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/support/FormattingConversionServiceAdapter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/support/FormattingConversionServiceAdapter.java @@ -19,79 +19,40 @@ package org.springframework.ui.format.support; import java.text.ParseException; import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.GenericConverter; -import org.springframework.ui.format.Formatter; -import org.springframework.ui.format.FormatterRegistry; -import org.springframework.util.Assert; +import org.springframework.ui.format.FormattingService; /** - * Adapter that exposes a {@link ConversionService} reference for a given - * {@link org.springframework.ui.format.FormatterRegistry}, retrieving the current - * Locale from {@link org.springframework.context.i18n.LocaleContextHolder}. + * Adapter that exposes a {@link ConversionService} reference for a given {@link FormattingService}, + * retrieving the current Locale from {@link LocaleContextHolder}. * * @author Juergen Hoeller * @since 3.0 */ public class FormattingConversionServiceAdapter extends GenericConversionService { - private final FormatterRegistry formatterRegistry; - - - /** - * Create a new FormattingConversionServiceAdapter for the given FormatterRegistry. - * @param formatterRegistry the FormatterRegistry to wrap - */ - public FormattingConversionServiceAdapter(FormatterRegistry formatterRegistry) { - Assert.notNull(formatterRegistry, "FormatterRegistry must not be null"); - this.formatterRegistry = formatterRegistry; - if (formatterRegistry instanceof GenericFormatterRegistry) { - setParent(((GenericFormatterRegistry) formatterRegistry).getConversionService()); - } - else { - setParent(new DefaultConversionService()); - } - } - - - @Override - protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { - if (String.class.equals(sourceType.getType())) { - Formatter formatter = this.formatterRegistry.getFormatter(targetType); - if (formatter != null) { - return new FormattingConverter(formatter); + private final FormattingService formattingService; + + public FormattingConversionServiceAdapter(FormattingService formattingService) { + this.formattingService = formattingService; + addGenericConverter(String.class, Object.class, new GenericConverter() { + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + try { + return FormattingConversionServiceAdapter.this.formattingService.parse((String) source, targetType, LocaleContextHolder.getLocale()); + } catch (ParseException e) { + throw new ConversionFailedException(sourceType, targetType, source, e); + } } - } - return super.getConverter(sourceType, targetType); - } - - - /** - * Adapter that exposes the Converter interface on top of a given Formatter. - */ - private static class FormattingConverter implements GenericConverter { - - private final Formatter formatter; - - public FormattingConverter(Formatter formatter) { - this.formatter = formatter; - } - - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - if (source == null) { - return null; + }); + addGenericConverter(Object.class, String.class, new GenericConverter() { + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return FormattingConversionServiceAdapter.this.formattingService.print(source, targetType, LocaleContextHolder.getLocale()); } - try { - return this.formatter.parse((String) source, LocaleContextHolder.getLocale()); - } - catch (ParseException ex) { - throw new IllegalArgumentException("Could not convert formatted value '" + source + "'", ex); - } - } - + }); } } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/support/FormattingPropertyEditorAdapter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/support/FormattingPropertyEditorAdapter.java index 039679fa1c7..1c02ed0e85e 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/support/FormattingPropertyEditorAdapter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/support/FormattingPropertyEditorAdapter.java @@ -20,7 +20,8 @@ import java.beans.PropertyEditorSupport; import java.text.ParseException; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.ui.format.Formatter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.ui.format.FormattingService; import org.springframework.util.Assert; /** @@ -33,23 +34,26 @@ import org.springframework.util.Assert; */ public class FormattingPropertyEditorAdapter extends PropertyEditorSupport { - private final Formatter formatter; - + private final FormattingService formattingService; + private final TypeDescriptor fieldType; + /** * Create a new FormattingPropertyEditorAdapter for the given Formatter. * @param formatter the Formatter to wrap */ - public FormattingPropertyEditorAdapter(Formatter formatter) { - Assert.notNull(formatter, "Formatter must not be null"); - this.formatter = formatter; + public FormattingPropertyEditorAdapter(FormattingService formattingService, Class fieldType) { + Assert.notNull(formattingService, "FormattingService must not be null"); + Assert.notNull(formattingService, "FieldType must not be null"); + this.formattingService = formattingService; + this.fieldType = TypeDescriptor.valueOf(fieldType); } @Override public void setAsText(String text) throws IllegalArgumentException { try { - setValue(this.formatter.parse(text, LocaleContextHolder.getLocale())); + setValue(this.formattingService.parse(text, this.fieldType, LocaleContextHolder.getLocale())); } catch (ParseException ex) { throw new IllegalArgumentException("Failed to parse formatted value", ex); @@ -58,7 +62,7 @@ public class FormattingPropertyEditorAdapter extends PropertyEditorSupport { @Override public String getAsText() { - return this.formatter.format(getValue(), LocaleContextHolder.getLocale()); + return this.formattingService.print(getValue(), this.fieldType, LocaleContextHolder.getLocale()); } } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/support/GenericFormatterRegistry.java b/org.springframework.context/src/main/java/org/springframework/ui/format/support/GenericFormatterRegistry.java deleted file mode 100644 index 5d7fefdc0eb..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/support/GenericFormatterRegistry.java +++ /dev/null @@ -1,406 +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.ui.format.support; - -import java.lang.annotation.Annotation; -import java.text.ParseException; -import java.util.LinkedList; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.springframework.beans.BeanUtils; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.GenericTypeResolver; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.ui.format.AnnotationFormatterFactory; -import org.springframework.ui.format.Formatted; -import org.springframework.ui.format.Formatter; -import org.springframework.ui.format.FormatterRegistry; -import org.springframework.util.Assert; - -/** - * A generic implementation of {@link org.springframework.ui.format.FormatterRegistry} - * suitable for use in most environments. - * - * @author Keith Donald - * @author Juergen Hoeller - * @since 3.0 - * @see #setFormatters(Set) - * @see #setFormatterMap(Map) - * @see #setAnnotationFormatterMap(Map) - * @see #setAnnotationFormatterFactories(Set) - * @see #setConversionService(ConversionService) - * @see #addFormatterByType(Formatter) - * @see #addFormatterByType(Class, Formatter) - * @see #addFormatterByAnnotation(Class, Formatter) - * @see #addFormatterByAnnotation(AnnotationFormatterFactory) - */ -public class GenericFormatterRegistry implements FormatterRegistry, ApplicationContextAware, Cloneable { - - private final Map, FormatterHolder> typeFormatters = new ConcurrentHashMap, FormatterHolder>(); - - private final Map, AnnotationFormatterFactoryHolder> annotationFormatters = new ConcurrentHashMap, AnnotationFormatterFactoryHolder>(); - - private ConversionService conversionService; - - private ApplicationContext applicationContext; - - private boolean shared = true; - - /** - * Registers the formatters in the set provided. - * JavaBean-friendly alternative to calling {@link #addFormatterByType(Formatter)}. - * @see #add(Formatter) - */ - public void setFormatters(Set> formatters) { - for (Formatter formatter : formatters) { - addFormatterByType(formatter); - } - } - - /** - * Registers the formatters in the map provided by type. - * JavaBean-friendly alternative to calling {@link #addFormatterByType(Class, Formatter)}. - * @see #add(Class, Formatter) - */ - public void setFormatterMap(Map, Formatter> formatters) { - for (Map.Entry, Formatter> entry : formatters.entrySet()) { - addFormatterByType(entry.getKey(), entry.getValue()); - } - } - - /** - * Registers the formatters in the map provided by annotation type. - * JavaBean-friendly alternative to calling {@link #addFormatterByAnnotation(Class, Formatter)}. - * @see #add(Class, Formatter) - */ - public void setAnnotationFormatterMap(Map, Formatter> formatters) { - for (Map.Entry, Formatter> entry : formatters.entrySet()) { - addFormatterByAnnotation(entry.getKey(), entry.getValue()); - } - } - - /** - * Registers the annotation formatter factories in the set provided. - * JavaBean-friendly alternative to calling {@link #addFormatterByAnnotation(AnnotationFormatterFactory)}. - * @see #add(AnnotationFormatterFactory) - */ - public void setAnnotationFormatterFactories(Set> factories) { - for (AnnotationFormatterFactory factory : factories) { - addFormatterByAnnotation(factory); - } - } - - /** - * Specify the type conversion service that will be used to coerce objects to the - * types required for formatting. Defaults to a {@link DefaultConversionService}. - * @see #addFormatterByType(Class, Formatter) - */ - public void setConversionService(ConversionService conversionService) { - Assert.notNull(conversionService, "ConversionService must not be null"); - this.conversionService = conversionService; - } - - /** - * Return the type conversion service which this FormatterRegistry delegates to. - */ - public ConversionService getConversionService() { - return this.conversionService; - } - - /** - * Take the context's default ConversionService if none specified locally. - */ - public void setApplicationContext(ApplicationContext context) { - if (this.conversionService == null - && context.containsBean(ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME)) { - this.conversionService = context.getBean(ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME, - ConversionService.class); - } - this.applicationContext = context; - } - - // cloning support - - /** - * Specify whether this FormatterRegistry is shared, in which case newly - * registered Formatters will be visible to other callers as well. - *

A new GenericFormatterRegistry is considered as shared by default, - * whereas a cloned GenericFormatterRegistry will be non-shared by default. - * @see #clone() - */ - public void setShared(boolean shared) { - this.shared = shared; - } - - /** - * Return whether this FormatterRegistry is shared, in which case newly - * registered Formatters will be visible to other callers as well. - */ - public boolean isShared() { - return this.shared; - } - - /** - * Create an independent clone of this FormatterRegistry. - * @see #setShared - */ - @Override - public GenericFormatterRegistry clone() { - GenericFormatterRegistry clone = new GenericFormatterRegistry(); - clone.typeFormatters.putAll(this.typeFormatters); - clone.annotationFormatters.putAll(this.annotationFormatters); - clone.conversionService = this.conversionService; - clone.applicationContext = applicationContext; - clone.shared = false; - return clone; - } - - // implementing FormatterRegistry - - public void addFormatterByType(Formatter formatter) { - Class formatterObjectType = GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class); - if (formatterObjectType == null) { - throw new IllegalArgumentException("Unable to register Formatter " + formatter - + "; cannot determine parameterized object type "); - } - this.typeFormatters.put(formatterObjectType, new FormatterHolder(formatterObjectType, formatter)); - } - - public void addFormatterByType(Class type, Formatter formatter) { - Class formatterObjectType = GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class); - if (formatterObjectType != null && !type.isAssignableFrom(formatterObjectType)) { - if (this.conversionService == null) { - throw new IllegalStateException("Unable to index Formatter " + formatter + " under type [" - + type.getName() + "]; not able to convert from a [" + formatterObjectType.getName() - + "] parsed by the Formatter to [" + type.getName() - + "] because this.conversionService is null"); - } - if (!this.conversionService.canConvert(formatterObjectType, type)) { - throw new IllegalArgumentException("Unable to index Formatter " + formatter + " under type [" - + type.getName() + "]; not able to convert from a [" + formatterObjectType.getName() - + "] parsed by the Formatter to [" + type.getName() + "]"); - } - if (!this.conversionService.canConvert(type, formatterObjectType)) { - throw new IllegalArgumentException("Unable to index Formatter " + formatter + " under type [" - + type.getName() + "]; not able to convert to [" + formatterObjectType.getName() - + "] to format a [" + type.getName() + "]"); - } - } - this.typeFormatters.put(type, new FormatterHolder(formatterObjectType, formatter)); - } - - public void addFormatterByAnnotation(Class annotationType, Formatter formatter) { - Class formatterObjectType = GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class); - SimpleAnnotationFormatterFactory factory = new SimpleAnnotationFormatterFactory(formatter); - this.annotationFormatters.put(annotationType, - new AnnotationFormatterFactoryHolder(formatterObjectType, factory)); - } - - public void addFormatterByAnnotation(AnnotationFormatterFactory factory) { - Class[] typeArgs = GenericTypeResolver.resolveTypeArguments(factory.getClass(), - AnnotationFormatterFactory.class); - if (typeArgs == null || typeArgs.length != 2) { - throw new IllegalArgumentException( - "Unable to extract parameterized type arguments from AnnotationFormatterFactory [" - + factory.getClass().getName() - + "]; does the factory parameterize the and generic types?"); - } - this.annotationFormatters.put(typeArgs[0], new AnnotationFormatterFactoryHolder(typeArgs[1], factory)); - } - - public Formatter getFormatter(TypeDescriptor type) { - Assert.notNull(type, "TypeDescriptor is required"); - FormatterHolder holder = findFormatterHolderForAnnotatedProperty(type.getAnnotations()); - Class objectType = type.getObjectType(); - if (holder == null) { - holder = findFormatterHolderForType(objectType); - } - if (holder == null) { - holder = getDefaultFormatterHolder(objectType); - } - if (holder == null) { - return null; - } - Class formatterObjectType = holder.getFormatterObjectType(); - if (formatterObjectType != null && !objectType.isAssignableFrom(formatterObjectType)) { - if (this.conversionService != null) { - return new ConvertingFormatter(type, holder); - } else { - return null; - } - } else { - return holder.getFormatter(); - } - } - - // internal helpers - - private FormatterHolder findFormatterHolderForAnnotatedProperty(Annotation[] annotations) { - for (Annotation annotation : annotations) { - FormatterHolder holder = findFormatterHolderForAnnotation(annotation); - if (holder != null) { - return holder; - } - } - return null; - } - - private FormatterHolder findFormatterHolderForAnnotation(Annotation annotation) { - Class annotationType = annotation.annotationType(); - AnnotationFormatterFactoryHolder factory = this.annotationFormatters.get(annotationType); - if (factory != null) { - return factory.getFormatterHolder(annotation); - } else { - Formatted formatted = annotationType.getAnnotation(Formatted.class); - if (formatted != null) { - // property annotation has @Formatted meta-annotation - Formatter formatter = createFormatter(formatted.value()); - addFormatterByAnnotation(annotationType, formatter); - return findFormatterHolderForAnnotation(annotation); - } else { - return null; - } - } - } - - private FormatterHolder findFormatterHolderForType(Class type) { - LinkedList> classQueue = new LinkedList>(); - classQueue.addFirst(type); - while (!classQueue.isEmpty()) { - Class currentClass = classQueue.removeLast(); - FormatterHolder holder = this.typeFormatters.get(currentClass); - if (holder != null) { - return holder; - } - if (currentClass.getSuperclass() != null) { - classQueue.addFirst(currentClass.getSuperclass()); - } - Class[] interfaces = currentClass.getInterfaces(); - for (Class ifc : interfaces) { - classQueue.addFirst(ifc); - } - } - return null; - } - - private FormatterHolder getDefaultFormatterHolder(Class type) { - Formatted formatted = AnnotationUtils.findAnnotation(type, Formatted.class); - if (formatted != null) { - Formatter formatter = createFormatter(formatted.value()); - addFormatterByType(type, formatter); - return findFormatterHolderForType(type); - } else { - return null; - } - } - - private Formatter createFormatter(Class formatterClass) { - return (this.applicationContext != null ? this.applicationContext.getAutowireCapableBeanFactory().createBean( - formatterClass) : BeanUtils.instantiate(formatterClass)); - } - - private abstract static class AbstractFormatterHolder { - - private Class formatterObjectType; - - public AbstractFormatterHolder(Class formatterObjectType) { - this.formatterObjectType = formatterObjectType; - } - - public Class getFormatterObjectType() { - return formatterObjectType; - } - - } - - private static class FormatterHolder extends AbstractFormatterHolder { - - private Formatter formatter; - - public FormatterHolder(Class formatterObjectType, Formatter formatter) { - super(formatterObjectType); - this.formatter = formatter; - } - - public Formatter getFormatter() { - return this.formatter; - } - - } - - private static class AnnotationFormatterFactoryHolder extends AbstractFormatterHolder { - - private AnnotationFormatterFactory factory; - - public AnnotationFormatterFactoryHolder(Class formatterObjectType, AnnotationFormatterFactory factory) { - super(formatterObjectType); - this.factory = factory; - } - - public FormatterHolder getFormatterHolder(Annotation annotation) { - return new FormatterHolder(getFormatterObjectType(), this.factory.getFormatter(annotation)); - } - - } - - private static class SimpleAnnotationFormatterFactory implements AnnotationFormatterFactory { - - private final Formatter instance; - - public SimpleAnnotationFormatterFactory(Formatter instance) { - this.instance = instance; - } - - public Formatter getFormatter(Annotation annotation) { - return this.instance; - } - } - - private class ConvertingFormatter implements Formatter { - - private final TypeDescriptor type; - - private final FormatterHolder formatterHolder; - - public ConvertingFormatter(TypeDescriptor type, FormatterHolder formatterHolder) { - this.type = type; - this.formatterHolder = formatterHolder; - } - - public String format(Object object, Locale locale) { - object = GenericFormatterRegistry.this.conversionService.convert(object, this.formatterHolder - .getFormatterObjectType()); - return this.formatterHolder.getFormatter().format(object, locale); - } - - public Object parse(String formatted, Locale locale) throws ParseException { - Object parsed = this.formatterHolder.getFormatter().parse(formatted, locale); - parsed = GenericFormatterRegistry.this.conversionService.convert(parsed, TypeDescriptor - .valueOf(this.formatterHolder.getFormatterObjectType()), this.type); - return parsed; - } - } - -} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/support/GenericFormattingService.java b/org.springframework.context/src/main/java/org/springframework/ui/format/support/GenericFormattingService.java new file mode 100644 index 00000000000..6bb5bcdcf8c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/support/GenericFormattingService.java @@ -0,0 +1,234 @@ +/* + * 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.ui.format.support; + +import java.lang.annotation.Annotation; +import java.text.ParseException; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.ui.format.AnnotationFormatterFactory; +import org.springframework.ui.format.Formatter; +import org.springframework.ui.format.FormatterRegistry; +import org.springframework.ui.format.FormattingService; +import org.springframework.ui.format.Parser; +import org.springframework.ui.format.Printer; +import org.springframework.util.Assert; + +/** + * A generic implementation of {@link FormattingService} suitable for use in most environments. + * Is a {@link FormatterRegistry} to allow for registration of field formatting logic. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +public class GenericFormattingService implements FormattingService, FormatterRegistry { + + private final Map, GenericFormatter> typeFormatters = new ConcurrentHashMap, GenericFormatter>(); + + private final Map, GenericAnnotationFormatterFactory> annotationFormatters = new ConcurrentHashMap, GenericAnnotationFormatterFactory>(); + + private GenericConversionService conversionService = new GenericConversionService(); + + /** + * Configure a parent of the type conversion service that will be used to coerce objects to types required for formatting. + */ + public void setParentConversionService(ConversionService parentConversionService) { + this.conversionService.setParent(parentConversionService); + } + + // implementing FormattingService + + public String print(Object fieldValue, TypeDescriptor fieldType, Locale locale) { + return getFormatter(fieldType).print(fieldValue, fieldType, locale); + } + + public Object parse(String submittedValue, TypeDescriptor fieldType, Locale locale) throws ParseException { + return getFormatter(fieldType).parse(submittedValue, fieldType, locale); + } + + // implementing FormatterRegistry + + public void addFormatterForFieldType(Class fieldType, Printer printer, Parser parser) { + Class printerObjectType = resolvePrinterObjectType(printer); + Class parserObjectType = resolveParserObjectType(parser); + this.typeFormatters.put(fieldType, new GenericFormatter(printerObjectType, printer, parserObjectType, parser)); + } + + public void addFormatterForFieldType(Class fieldType, Formatter formatter) { + Class formatterObjectType = resolveFormatterObjectType(formatter); + this.typeFormatters.put(fieldType, new GenericFormatter(formatterObjectType, formatter, formatterObjectType, formatter)); + } + + public void addFormatterForFieldAnnotation(AnnotationFormatterFactory annotationFormatterFactory) { + Class annotationType = resolveAnnotationType(annotationFormatterFactory); + if (annotationType == null) { + throw new IllegalArgumentException( + "Unable to extract parameterized Annotation type argument from AnnotationFormatterFactory [" + + annotationFormatterFactory.getClass().getName() + + "]; does the factory parameterize the generic type?"); + } + this.annotationFormatters.put(annotationType, new GenericAnnotationFormatterFactory(annotationFormatterFactory)); + } + + public ConverterRegistry getConverterRegistry() { + return this.conversionService; + } + + // internal helpers + + private Class resolveParserObjectType(Parser parser) { + return GenericTypeResolver.resolveTypeArgument(parser.getClass(), Parser.class); + } + + private Class resolvePrinterObjectType(Printer printer) { + return GenericTypeResolver.resolveTypeArgument(printer.getClass(), Printer.class); + } + + private Class resolveFormatterObjectType(Formatter formatter) { + return GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class); + } + + @SuppressWarnings("unchecked") + private Class resolveAnnotationType(AnnotationFormatterFactory annotationFormatterFactory) { + return (Class) GenericTypeResolver.resolveTypeArgument(annotationFormatterFactory.getClass(), AnnotationFormatterFactory.class); + } + + private GenericFormatter getFormatter(TypeDescriptor fieldType) { + Assert.notNull(fieldType, "Field TypeDescriptor is required"); + GenericFormatter formatter = findFormatterForAnnotatedField(fieldType); + Class fieldObjectType = fieldType.getObjectType(); + if (formatter == null) { + formatter = findFormatterForFieldType(fieldObjectType); + } + return formatter; + } + + private GenericFormatter findFormatterForAnnotatedField(TypeDescriptor fieldType) { + for (Annotation annotation : fieldType.getAnnotations()) { + GenericFormatter formatter = findFormatterForAnnotation(annotation, fieldType.getObjectType()); + if (formatter != null) { + return formatter; + } + } + return null; + } + + private GenericFormatter findFormatterForAnnotation(Annotation annotation, Class fieldType) { + Class annotationType = annotation.annotationType(); + GenericAnnotationFormatterFactory factory = this.annotationFormatters.get(annotationType); + if (factory != null) { + return factory.getFormatter(annotation, fieldType); + } else { + return null; + } + } + + private GenericFormatter findFormatterForFieldType(Class fieldType) { + LinkedList> classQueue = new LinkedList>(); + classQueue.addFirst(fieldType); + while (!classQueue.isEmpty()) { + Class currentClass = classQueue.removeLast(); + GenericFormatter formatter = this.typeFormatters.get(currentClass); + if (formatter != null) { + return formatter; + } + if (currentClass.getSuperclass() != null) { + classQueue.addFirst(currentClass.getSuperclass()); + } + Class[] interfaces = currentClass.getInterfaces(); + for (Class ifc : interfaces) { + classQueue.addFirst(ifc); + } + } + return null; + } + + @SuppressWarnings("unchecked") + private class GenericFormatter { + + private TypeDescriptor printerObjectType; + + private Printer printer; + + private Parser parser; + + public GenericFormatter(Class printerObjectType, Printer printer, Class parserObjectType, Parser parser) { + this.printerObjectType = TypeDescriptor.valueOf(printerObjectType); + this.printer = printer; + this.parser = parser; + } + + public String print(Object fieldValue, TypeDescriptor fieldType, Locale locale) { + if (!fieldType.isAssignableTo(this.printerObjectType)) { + fieldValue = GenericFormattingService.this.conversionService.convert(fieldValue, fieldType, this.printerObjectType); + } + return fieldType != null ? this.printer.print(fieldValue, locale) : ""; + } + + public Object parse(String submittedValue, TypeDescriptor fieldType, Locale locale) throws ParseException { + if (submittedValue.isEmpty()) { + return null; + } + Object parsedValue = this.parser.parse(submittedValue, locale); + TypeDescriptor parsedObjectType = TypeDescriptor.valueOf(parsedValue.getClass()); + if (!parsedObjectType.isAssignableTo(fieldType)) { + parsedValue = GenericFormattingService.this.conversionService.convert(parsedValue, parsedObjectType, fieldType); + } + return parsedValue; + } + + } + + @SuppressWarnings("unchecked") + private class GenericAnnotationFormatterFactory { + + private AnnotationFormatterFactory annotationFormatterFactory; + + public GenericAnnotationFormatterFactory(AnnotationFormatterFactory annotationFormatterFactory) { + this.annotationFormatterFactory = annotationFormatterFactory; + } + + public GenericFormatter getFormatter(Annotation annotation, Class fieldType) { + Printer printer = this.annotationFormatterFactory.getPrinter(annotation, fieldType); + Parser parser = this.annotationFormatterFactory.getParser(annotation, fieldType); + return new GenericFormatter(getPrinterObjectType(printer, fieldType), printer, getParserObjectType(parser, fieldType), parser); + } + + // internal helpers + + private Class getPrinterObjectType(Printer printer, Class fieldType) { + // TODO cache + return resolvePrinterObjectType(printer); + } + + private Class getParserObjectType(Parser parser, Class fieldType) { + // TODO cache + return resolveParserObjectType(parser); + } + + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/support/MethodInvokingFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/support/MethodInvokingFormatter.java deleted file mode 100644 index 9f17d6f148b..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/support/MethodInvokingFormatter.java +++ /dev/null @@ -1,115 +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.ui.format.support; - -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Locale; - -import org.springframework.ui.format.Formatter; -import org.springframework.util.ReflectionUtils; - -/** - * A generic Formatter that reflectively invokes methods on a class to carry out format and parse operations. - * The format method should be a public member method that returns a String and either accepts no arguments or a single Locale argument. - * The parse method should be a declared public static method that returns the formattedObjectType and either accepts a single String argument or String and Locale arguments. - * Throws an {@link IllegalArgumentException} if either the format method or parse method could not be resolved. - * Useful for invoking existing Formatter logic on a formattable type without needing a dedicated Formatter class. - * - * @author Keith Donald - */ -public class MethodInvokingFormatter implements Formatter { - - private Class formattedObjectType; - - private Method formatMethod; - - private boolean formatMethodLocaleArgumentPresent; - - private Method parseMethod; - - private boolean parseMethodLocaleArgumentPresent; - - /** - * Creates a new reflective method invoking formatter. - * @param formattedObjectType the object type that contains format and parse methods - * @param formatMethodName the format method name e.g. "toString" - * @param parseMethodName the parse method name e.g. "valueOf" - */ - public MethodInvokingFormatter(Class formattedObjectType, String formatMethodName, String parseMethodName) { - this.formattedObjectType = formattedObjectType; - resolveFormatMethod(formatMethodName); - resolveParseMethod(parseMethodName); - } - - public String format(Object object, Locale locale) { - if (this.formatMethodLocaleArgumentPresent) { - return (String) ReflectionUtils.invokeMethod(this.formatMethod, object, locale); - } else { - return (String) ReflectionUtils.invokeMethod(this.formatMethod, object); - } - } - - public Object parse(String formatted, Locale locale) { - if (this.parseMethodLocaleArgumentPresent) { - return ReflectionUtils.invokeMethod(this.parseMethod, null, formatted, locale); - } else { - return ReflectionUtils.invokeMethod(this.parseMethod, null, formatted); - } - } - - private void resolveFormatMethod(String methodName) { - Method[] methods = this.formattedObjectType.getMethods(); - for (Method method : methods) { - if (method.getName().equals(methodName) && method.getReturnType().equals(String.class)) { - if (method.getParameterTypes().length == 0) { - this.formatMethod = method; - } else if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(Locale.class)) { - this.formatMethod = method; - this.formatMethodLocaleArgumentPresent = true; - } - } - } - if (this.formatMethod == null) { - throw new IllegalArgumentException("Unable to resolve format method '" + methodName + "' on class [" - + this.formattedObjectType.getName() - + "] method should have signature [public String ()] " - + "or [public String (Locale)]"); - } - } - - private void resolveParseMethod(String methodName) { - Method[] methods = this.formattedObjectType.getDeclaredMethods(); - for (Method method : methods) { - if (method.getName().equals(methodName) && method.getReturnType().equals(this.formattedObjectType) - && Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) { - if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(String.class)) { - this.parseMethod = method; - } else if (method.getParameterTypes().length == 2 && method.getParameterTypes()[0].equals(String.class) - && method.getParameterTypes()[1].equals(Locale.class)) { - this.parseMethod = method; - this.parseMethodLocaleArgumentPresent = true; - } - } - } - if (this.parseMethod == null) { - throw new IllegalArgumentException("Unable to resolve parse method '" + methodName + "' on class [" - + this.formattedObjectType.getName() - + "]; method should have signature [public static T (String)] " - + "or [public static T (String, Locale)]"); - } - } -} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java b/org.springframework.context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java index d6eaee8a896..1067c8de532 100644 --- a/org.springframework.context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java +++ b/org.springframework.context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java @@ -24,8 +24,7 @@ import org.springframework.beans.PropertyAccessorUtils; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.ui.format.Formatter; -import org.springframework.ui.format.FormatterRegistry; +import org.springframework.ui.format.FormattingService; import org.springframework.ui.format.support.FormattingConversionServiceAdapter; import org.springframework.ui.format.support.FormattingPropertyEditorAdapter; import org.springframework.util.Assert; @@ -44,7 +43,7 @@ import org.springframework.util.Assert; */ public abstract class AbstractPropertyBindingResult extends AbstractBindingResult { - private FormatterRegistry formatterRegistry; + private FormattingService formattingService; /** @@ -57,10 +56,10 @@ public abstract class AbstractPropertyBindingResult extends AbstractBindingResul } - public void initFormatterLookup(FormatterRegistry formatterRegistry) { - Assert.notNull(formatterRegistry, "FormatterRegistry must not be null"); - this.formatterRegistry = formatterRegistry; - getPropertyAccessor().setConversionService(new FormattingConversionServiceAdapter(formatterRegistry)); + public void initFormatting(FormattingService formattingService) { + Assert.notNull(formattingService, "FormattingService must not be null"); + this.formattingService = formattingService; + getPropertyAccessor().setConversionService(new FormattingConversionServiceAdapter(formattingService)); } /** @@ -117,14 +116,13 @@ public abstract class AbstractPropertyBindingResult extends AbstractBindingResul return textValue; } } - // Try custom formatter... - TypeDescriptor td = getPropertyAccessor().getPropertyTypeDescriptor(fixedField); - Formatter formatter = (this.formatterRegistry != null ? this.formatterRegistry.getFormatter(td) : null); - if (formatter != null) { - return formatter.format(value, LocaleContextHolder.getLocale()); + if (this.formattingService != null) { + // Try custom formatter... + TypeDescriptor td = getPropertyAccessor().getPropertyTypeDescriptor(fixedField); + return this.formattingService.print(value, td, LocaleContextHolder.getLocale()); + } else { + return value; } - // Nothing found: return value as-is. - return value; } /** @@ -147,16 +145,15 @@ public abstract class AbstractPropertyBindingResult extends AbstractBindingResul */ @Override public PropertyEditor findEditor(String field, Class valueType) { + if (valueType == null) { + valueType = getFieldType(field); + } PropertyEditor editor = super.findEditor(field, valueType); if (editor == null) { TypeDescriptor td = (field != null ? getPropertyAccessor().getPropertyTypeDescriptor(fixedField(field)) : TypeDescriptor.valueOf(valueType)); - Formatter formatter = - (this.formatterRegistry != null ? this.formatterRegistry.getFormatter(td) : null); - if (formatter != null) { - editor = new FormattingPropertyEditorAdapter(formatter); - } + editor = new FormattingPropertyEditorAdapter(this.formattingService, valueType); } return editor; } diff --git a/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java b/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java index f11deec4c1d..73f1128f69c 100644 --- a/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java +++ b/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java @@ -22,7 +22,6 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.ConfigurablePropertyAccessor; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyAccessException; @@ -35,10 +34,8 @@ import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.ui.format.FormatterRegistry; +import org.springframework.ui.format.FormattingService; import org.springframework.ui.format.support.FormattingConversionServiceAdapter; -import org.springframework.ui.format.support.GenericFormatterRegistry; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; @@ -138,7 +135,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { private Validator validator; - private FormatterRegistry formatterRegistry; + private FormattingService formattingService; /** @@ -186,8 +183,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { Assert.isNull(this.bindingResult, "DataBinder is already initialized - call initBeanPropertyAccess before any other configuration methods"); this.bindingResult = new BeanPropertyBindingResult(getTarget(), getObjectName()); - if (this.formatterRegistry != null) { - this.bindingResult.initFormatterLookup(this.formatterRegistry); + if (this.formattingService != null) { + this.bindingResult.initFormatting(this.formattingService); } } @@ -200,8 +197,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { Assert.isNull(this.bindingResult, "DataBinder is already initialized - call initDirectFieldAccess before any other configuration methods"); this.bindingResult = new DirectFieldBindingResult(getTarget(), getObjectName()); - if (this.formatterRegistry != null) { - this.bindingResult.initFormatterLookup(this.formatterRegistry); + if (this.formattingService != null) { + this.bindingResult.initFormatting(this.formattingService); } } @@ -229,8 +226,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { protected SimpleTypeConverter getSimpleTypeConverter() { if (this.typeConverter == null) { this.typeConverter = new SimpleTypeConverter(); - if (this.formatterRegistry != null) { - this.typeConverter.setConversionService(new FormattingConversionServiceAdapter(this.formatterRegistry)); + if (this.formattingService != null) { + this.typeConverter.setConversionService(new FormattingConversionServiceAdapter(this.formattingService)); } } return this.typeConverter; @@ -464,33 +461,12 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { } /** - * Set the FormatterRegistry to use for obtaining Formatters in preference - * to JavaBeans PropertyEditors. + * Set the FormattingService to use for field value formatting in preference to JavaBeans PropertyEditors. */ - public void setFormatterRegistry(FormatterRegistry formatterRegistry) { - this.formatterRegistry = formatterRegistry; + public void setFormattingService(FormattingService formattingService) { + this.formattingService = formattingService; } - /** - * Return the FormatterRegistry to use for obtaining Formatters in preference - * to JavaBeans PropertyEditors. - * @return the FormatterRegistry (never null), which may also be - * used to register further Formatters for this DataBinder - */ - public FormatterRegistry getFormatterRegistry() { - if (this.formatterRegistry == null) { - GenericFormatterRegistry registry = new GenericFormatterRegistry(); - registry.setConversionService(new DefaultConversionService()); - this.formatterRegistry = registry; - } - else if (this.formatterRegistry instanceof GenericFormatterRegistry && - ((GenericFormatterRegistry) this.formatterRegistry).isShared()) { - this.formatterRegistry = ((GenericFormatterRegistry) this.formatterRegistry).clone(); - } - return this.formatterRegistry; - } - - //--------------------------------------------------------------------- // Implementation of PropertyEditorRegistry/TypeConverter interface //--------------------------------------------------------------------- diff --git a/org.springframework.context/src/test/java/org/springframework/mapping/support/MappingTests.java b/org.springframework.context/src/test/java/org/springframework/mapping/support/MappingTests.java index 3085002b4de..080e6412de4 100644 --- a/org.springframework.context/src/test/java/org/springframework/mapping/support/MappingTests.java +++ b/org.springframework.context/src/test/java/org/springframework/mapping/support/MappingTests.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import org.joda.time.DateTime; +import org.joda.time.MutableDateTime; import org.joda.time.format.ISODateTimeFormat; import org.junit.Test; import org.springframework.core.convert.converter.Converter; @@ -317,6 +318,35 @@ public class MappingTests { .getActivationDateTime()); } + @Test + public void testMultiFieldToFieldMappingWithAssembler() { + Mapper mapper = MapperFactory.mapperBuilder(Map.class, Account.class) + .setAutoMappingEnabled(false) + // field to multiple fields + .addAssemblerMapping("activationDateTime", new Converter, DateTime>() { + public DateTime convert(Map source) { + MutableDateTime dateTime = new MutableDateTime(); + dateTime.setYear(Integer.parseInt(source.get("year"))); + dateTime.setMonthOfYear(Integer.parseInt(source.get("month"))); + dateTime.setDayOfMonth(Integer.parseInt(source.get("day"))); + dateTime.setHourOfDay(Integer.parseInt(source.get("hour"))); + dateTime.setMinuteOfHour(Integer.parseInt(source.get("minute"))); + dateTime.setSecondOfMinute(0); + dateTime.setMillisOfSecond(0); + return dateTime.toDateTime(); + } + }).getMapper(); + Map source = new HashMap(); + source.put("activationDateTime.year", "2009"); + source.put("activationDateTime.month", "10"); + source.put("activationDateTime.day", "12"); + source.put("activationDateTime.hour", "12"); + source.put("activationDateTime.minute", "0"); + Account account = mapper.map(source, new Account()); + assertEquals(ISODateTimeFormat.dateTime().parseDateTime("2009-10-12T12:00:00.000-04:00"), account + .getActivationDateTime()); + } + @Test public void conditionalMapping() { Map domestic = new HashMap(); @@ -328,17 +358,16 @@ public class MappingTests { domestic.put("cityCode", "whatever"); Mapper mapper = MapperFactory.mapperBuilder(Map.class, PhoneNumber.class) - .addConditionalMapping("countryCode", "international == 'true'") - .addConditionalMapping("cityCode", "international == 'true'") - .getMapper(); - + .addConditionalMapping("countryCode", "international == 'true'").addConditionalMapping("cityCode", + "international == 'true'").getMapper(); + PhoneNumber number = mapper.map(domestic, new PhoneNumber()); assertEquals("205", number.getAreaCode()); assertEquals("339", number.getPrefix()); assertEquals("1234", number.getLine()); assertNull(number.getCountryCode()); assertNull(number.getCityCode()); - + Map international = new HashMap(); international.put("international", "true"); international.put("areaCode", "205"); @@ -346,7 +375,7 @@ public class MappingTests { international.put("line", "1234"); international.put("countryCode", "1"); international.put("cityCode", "2"); - + PhoneNumber number2 = mapper.map(international, new PhoneNumber()); assertEquals("205", number2.getAreaCode()); @@ -479,7 +508,7 @@ public class MappingTests { MapperFactory.defaultMapper().map(source, target); assertEquals(1, target.getNumber()); - assertTrue(item != target.getLineItem()); + assertTrue(item == target.getLineItem()); assertEquals(new BigDecimal("30.00"), target.getLineItem().getAmount()); assertEquals(source, target.getLineItem().getOrder()); } diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/date/DateFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/date/DateFormatterTests.java index 4214838d7e4..980e3e0b054 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/format/date/DateFormatterTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/format/date/DateFormatterTests.java @@ -37,7 +37,7 @@ public class DateFormatterTests { cal.set(Calendar.YEAR, 2009); cal.set(Calendar.MONTH, Calendar.JUNE); cal.set(Calendar.DAY_OF_MONTH, 1); - assertEquals("2009-06-01", formatter.format(cal.getTime(), Locale.US)); + assertEquals("2009-06-01", formatter.print(cal.getTime(), Locale.US)); } @Test diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/number/CurrencyFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/number/CurrencyFormatterTests.java index 3c16afe69fe..709a721b166 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/format/number/CurrencyFormatterTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/format/number/CurrencyFormatterTests.java @@ -16,12 +16,13 @@ package org.springframework.ui.format.number; +import static org.junit.Assert.assertEquals; + import java.math.BigDecimal; import java.math.RoundingMode; import java.text.ParseException; import java.util.Locale; -import static org.junit.Assert.*; import org.junit.Test; /** @@ -33,7 +34,7 @@ public class CurrencyFormatterTests { @Test public void formatValue() { - assertEquals("$23.00", formatter.format(new BigDecimal("23"), Locale.US)); + assertEquals("$23.00", formatter.print(new BigDecimal("23"), Locale.US)); } @Test @@ -41,11 +42,6 @@ public class CurrencyFormatterTests { assertEquals(new BigDecimal("23.56"), formatter.parse("$23.56", Locale.US)); } - @Test - public void parseEmptyValue() throws ParseException { - assertEquals(null, formatter.parse("", Locale.US)); - } - @Test(expected = ParseException.class) public void parseBogusValue() throws ParseException { formatter.parse("bogus", Locale.US); diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/number/DecimalFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/number/DecimalFormatterTests.java index 0e4bdf7de7c..0c200ea950a 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/format/number/DecimalFormatterTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/format/number/DecimalFormatterTests.java @@ -34,7 +34,7 @@ public class DecimalFormatterTests { @Test public void formatValue() { - assertEquals("23.56", formatter.format(new BigDecimal("23.56"), Locale.US)); + assertEquals("23.56", formatter.print(new BigDecimal("23.56"), Locale.US)); } @Test @@ -42,11 +42,6 @@ public class DecimalFormatterTests { assertEquals(new BigDecimal("23.56"), formatter.parse("23.56", Locale.US)); } - @Test - public void parseEmptyValue() throws ParseException { - assertEquals(null, formatter.parse("", Locale.US)); - } - @Test(expected = ParseException.class) public void parseBogusValue() throws ParseException { formatter.parse("bogus", Locale.US); diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/number/IntegerFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/number/IntegerFormatterTests.java index 7d39faa0119..ca277833edc 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/format/number/IntegerFormatterTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/format/number/IntegerFormatterTests.java @@ -33,7 +33,7 @@ public class IntegerFormatterTests { @Test public void formatValue() { - assertEquals("23", formatter.format(23L, Locale.US)); + assertEquals("23", formatter.print(23L, Locale.US)); } @Test @@ -41,11 +41,6 @@ public class IntegerFormatterTests { assertEquals((Long) 2356L, formatter.parse("2356", Locale.US)); } - @Test - public void parseEmptyValue() throws ParseException { - assertEquals(null, formatter.parse("", Locale.US)); - } - @Test(expected = ParseException.class) public void parseBogusValue() throws ParseException { formatter.parse("bogus", Locale.US); diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/number/PercentFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/number/PercentFormatterTests.java index dac95522f21..d770a56e796 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/format/number/PercentFormatterTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/format/number/PercentFormatterTests.java @@ -34,7 +34,7 @@ public class PercentFormatterTests { @Test public void formatValue() { - assertEquals("23%", formatter.format(new BigDecimal(".23"), Locale.US)); + assertEquals("23%", formatter.print(new BigDecimal(".23"), Locale.US)); } @Test @@ -43,11 +43,6 @@ public class PercentFormatterTests { Locale.US)); } - @Test - public void parseEmptyValue() throws ParseException { - assertEquals(null, formatter.parse("", Locale.US)); - } - @Test(expected = ParseException.class) public void parseBogusValue() throws ParseException { formatter.parse("bogus", Locale.US); diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/support/GenericFormatterRegistryTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/support/GenericFormatterRegistryTests.java deleted file mode 100644 index 819eadf1406..00000000000 --- a/org.springframework.context/src/test/java/org/springframework/ui/format/support/GenericFormatterRegistryTests.java +++ /dev/null @@ -1,228 +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.ui.format.support; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.text.ParseException; -import java.util.Locale; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.style.ToStringCreator; -import org.springframework.ui.format.AnnotationFormatterFactory; -import org.springframework.ui.format.Formatted; -import org.springframework.ui.format.Formatter; -import org.springframework.ui.format.number.CurrencyFormatter; -import org.springframework.ui.format.number.IntegerFormatter; -import org.springframework.ui.format.support.GenericFormatterRegistry; - -/** - * @author Keith Donald - * @author Juergen Hoeller - */ -public class GenericFormatterRegistryTests { - - private GenericFormatterRegistry registry; - - @Before - public void setUp() { - registry = new GenericFormatterRegistry(); - registry.setConversionService(new DefaultConversionService()); - } - - @Test - public void testAdd() throws ParseException { - registry.addFormatterByType(new IntegerFormatter()); - Formatter formatter = registry.getFormatter(TypeDescriptor.valueOf(Integer.class)); - String formatted = formatter.format(new Integer(3), Locale.US); - assertEquals("3", formatted); - Integer i = (Integer) formatter.parse("3", Locale.US); - assertEquals(new Integer(3), i); - } - - @Test - public void testAddLookupByPrimitive() throws ParseException { - registry.addFormatterByType(new IntegerFormatter()); - Formatter formatter = registry.getFormatter(TypeDescriptor.valueOf(int.class)); - String formatted = formatter.format(3, Locale.US); - assertEquals("3", formatted); - int integer = (Integer) formatter.parse("3", Locale.US); - assertEquals(3, integer); - } - - @Test - public void testAddByObjectType() { - registry.addFormatterByType(BigInteger.class, new IntegerFormatter()); - Formatter formatter = registry.getFormatter(TypeDescriptor.valueOf(BigInteger.class)); - String formatted = formatter.format(new BigInteger("3"), Locale.US); - assertEquals("3", formatted); - } - - @Test - public void testAddByAnnotation() throws Exception { - registry.addFormatterByAnnotation(Currency.class, new CurrencyFormatter()); - Formatter formatter = registry.getFormatter(new TypeDescriptor(getClass().getField("currencyField"))); - String formatted = formatter.format(new BigDecimal("5.00"), Locale.US); - assertEquals("$5.00", formatted); - } - - @Test - public void testAddAnnotationFormatterFactory() throws Exception { - registry.addFormatterByAnnotation(new CurrencyAnnotationFormatterFactory()); - Formatter formatter = registry.getFormatter(new TypeDescriptor(getClass().getField("currencyField"))); - String formatted = formatter.format(new BigDecimal("5.00"), Locale.US); - assertEquals("$5.00", formatted); - } - - @Test - public void testGetDefaultFormatterFromMetaAnnotation() throws Exception { - Formatter formatter = registry.getFormatter(new TypeDescriptor(getClass().getField("smartCurrencyField"))); - String formatted = formatter.format(new BigDecimal("5.00"), Locale.US); - assertEquals("$5.00", formatted); - } - - @Test - public void testGetDefaultFormatterForType() { - Formatter formatter = registry.getFormatter(TypeDescriptor.valueOf(Address.class)); - Address address = new Address(); - address.street = "12345 Bel Aire Estates"; - address.city = "Palm Bay"; - address.state = "FL"; - address.zip = "12345"; - String formatted = formatter.format(address, Locale.US); - assertEquals("12345 Bel Aire Estates:Palm Bay:FL:12345", formatted); - } - - @Test - public void testGetDefaultFormatterNull() throws ParseException { - assertNull(registry.getFormatter(TypeDescriptor.valueOf(Integer.class))); - } - - @Test(expected = IllegalArgumentException.class) - public void testGetFormatterCannotConvert() { - registry.addFormatterByType(Integer.class, new AddressFormatter()); - } - - @Currency - public BigDecimal currencyField; - - @SmartCurrency - public BigDecimal smartCurrencyField; - - @Target( { ElementType.METHOD, ElementType.FIELD }) - @Retention(RetentionPolicy.RUNTIME) - public @interface Currency { - } - - @Target( { ElementType.METHOD, ElementType.FIELD }) - @Retention(RetentionPolicy.RUNTIME) - @Formatted(CurrencyFormatter.class) - public @interface SmartCurrency { - } - - public static class CurrencyAnnotationFormatterFactory implements AnnotationFormatterFactory { - - private final CurrencyFormatter currencyFormatter = new CurrencyFormatter(); - - public Formatter getFormatter(Currency annotation) { - return this.currencyFormatter; - } - } - - @Formatted(AddressFormatter.class) - public static class Address { - - private String street; - private String city; - private String state; - private String zip; - private String country; - - public String getStreet() { - return street; - } - - public void setStreet(String street) { - this.street = street; - } - - public String getCity() { - return city; - } - - 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; - } - - public String getCountry() { - return country; - } - - public void setCountry(String country) { - this.country = country; - } - - public String toString() { - return new ToStringCreator(this).append("street", street).append("city", city).append("state", state) - .append("zip", zip).toString(); - } - } - - public static class AddressFormatter implements Formatter
{ - - public String format(Address address, Locale locale) { - return address.getStreet() + ":" + address.getCity() + ":" + address.getState() + ":" + address.getZip(); - } - - public Address parse(String formatted, Locale locale) throws ParseException { - Address address = new Address(); - String[] fields = formatted.split(":"); - address.setStreet(fields[0]); - address.setCity(fields[1]); - address.setState(fields[2]); - address.setZip(fields[3]); - return address; - } - } - -} diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/support/GenericFormattingServiceTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/support/GenericFormattingServiceTests.java new file mode 100644 index 00000000000..e76519a4bea --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/ui/format/support/GenericFormattingServiceTests.java @@ -0,0 +1,105 @@ +/* + * 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.ui.format.support; + +import static org.junit.Assert.assertEquals; + +import java.text.ParseException; +import java.util.Date; +import java.util.Locale; + +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormat; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.ui.format.jodatime.DateTimeFormatAnnotationFormatterFactory; +import org.springframework.ui.format.jodatime.DateTimeParser; +import org.springframework.ui.format.jodatime.ReadablePartialPrinter; +import org.springframework.ui.format.jodatime.DateTimeFormat.FormatStyle; +import org.springframework.ui.format.number.IntegerFormatter; + +/** + * @author Keith Donald + * @author Juergen Hoeller + */ +public class GenericFormattingServiceTests { + + private GenericFormattingService formattingService; + + @Before + public void setUp() { + formattingService = new GenericFormattingService(); + formattingService.setParentConversionService(new DefaultConversionService()); + } + + @Test + public void testFormatFieldForTypeWithFormatter() throws ParseException { + formattingService.addFormatterForFieldType(Number.class, new IntegerFormatter()); + String formatted = formattingService.print(new Integer(3), TypeDescriptor.valueOf(Integer.class), Locale.US); + assertEquals("3", formatted); + Integer i = (Integer) formattingService.parse("3", TypeDescriptor.valueOf(Integer.class), Locale.US); + assertEquals(new Integer(3), i); + } + + @Test + public void testFormatFieldForTypeWithPrinterParserWithCoersion() throws ParseException { + formattingService.getConverterRegistry().addConverter(new Converter() { + public LocalDate convert(DateTime source) { + return source.toLocalDate(); + } + }); + formattingService.addFormatterForFieldType(LocalDate.class, new ReadablePartialPrinter(DateTimeFormat + .shortDate()), new DateTimeParser(DateTimeFormat.shortDate())); + String formatted = formattingService.print(new LocalDate(2009, 10, 31), TypeDescriptor.valueOf(LocalDate.class), Locale.US); + assertEquals("10/31/09", formatted); + LocalDate date = (LocalDate) formattingService.parse("10/31/09", TypeDescriptor.valueOf(LocalDate.class), Locale.US); + assertEquals(new LocalDate(2009, 10, 31), date); + } + + @Test + public void testFormatFieldForAnnotation() throws Exception { + formattingService.getConverterRegistry().addConverter(new Converter() { + public Long convert(Date source) { + return source.getTime(); + } + }); + formattingService.getConverterRegistry().addConverter(new Converter() { + public Date convert(DateTime source) { + return source.toDate(); + } + }); + formattingService.addFormatterForFieldAnnotation(new DateTimeFormatAnnotationFormatterFactory()); + String formatted = formattingService.print(new LocalDate(2009, 10, 31).toDateTimeAtCurrentTime().toDate(), new TypeDescriptor(Model.class.getField("date")), Locale.US); + assertEquals("10/31/09", formatted); + LocalDate date = new LocalDate(formattingService.parse("10/31/09", new TypeDescriptor(Model.class.getField("date")), Locale.US)); + assertEquals(new LocalDate(2009, 10, 31), date); + } + + private static class Model { + + @SuppressWarnings("unused") + @org.springframework.ui.format.jodatime.DateTimeFormat(dateStyle=FormatStyle.SHORT) + public Date date; + + } + + +} diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/support/MethodInvokingFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/support/MethodInvokingFormatterTests.java deleted file mode 100644 index 6822f42ab09..00000000000 --- a/org.springframework.context/src/test/java/org/springframework/ui/format/support/MethodInvokingFormatterTests.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.springframework.ui.format.support; - -import static org.junit.Assert.assertEquals; - -import java.util.Locale; - -import org.junit.Test; -import org.springframework.ui.format.support.MethodInvokingFormatter; - -public class MethodInvokingFormatterTests { - - private MethodInvokingFormatter formatter = new MethodInvokingFormatter(AccountNumber.class, "getFormatted", - "valueOf"); - - private MethodInvokingFormatter formatter2 = new MethodInvokingFormatter(I8nAccountNumber.class, "getFormatted", - "valueOf"); - - @Test - public void testFormat() { - assertEquals("123456789", formatter.format(new AccountNumber(123456789L), null)); - } - - @Test - public void testParse() { - assertEquals(new Long(123456789), ((AccountNumber) formatter.parse("123456789", null)).number); - } - - @Test - public void testFormatI18n() { - assertEquals("123456789", formatter2.format(new I8nAccountNumber(123456789L), Locale.GERMAN)); - } - - @Test - public void testParseI18n() { - assertEquals(new Long(123456789), ((I8nAccountNumber) formatter2.parse("123456789", Locale.GERMAN)).number); - } - - public static class AccountNumber { - - private Long number; - - public AccountNumber(Long number) { - this.number = number; - } - - public String getFormatted() { - return number.toString(); - } - - public static AccountNumber valueOf(String str) { - return new AccountNumber(Long.valueOf(str)); - } - - } - - public static class I8nAccountNumber { - - private Long number; - - public I8nAccountNumber(Long number) { - this.number = number; - } - - public String getFormatted(Locale locale) { - assertEquals(Locale.GERMAN, locale); - return number.toString(); - } - - public static I8nAccountNumber valueOf(String str, Locale locale) { - assertEquals(Locale.GERMAN, locale); - return new I8nAccountNumber(Long.valueOf(str)); - } - - } -} diff --git a/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java b/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java index 38d3707218c..199967f5b27 100644 --- a/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -16,21 +16,20 @@ package org.springframework.validation; -import java.beans.PropertyEditorSupport; import java.beans.PropertyEditor; +import java.beans.PropertyEditorSupport; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Retention; -import java.lang.annotation.ElementType; -import java.lang.annotation.Target; -import java.math.BigDecimal; import junit.framework.TestCase; @@ -45,12 +44,12 @@ import org.springframework.beans.SerializablePerson; import org.springframework.beans.TestBean; import org.springframework.beans.propertyeditors.CustomCollectionEditor; import org.springframework.beans.propertyeditors.CustomNumberEditor; +import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.context.support.StaticMessageSource; -import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.ui.format.number.DecimalFormatter; -import org.springframework.ui.format.Formatted; -import org.springframework.ui.format.support.GenericFormatterRegistry; +import org.springframework.ui.format.support.GenericFormattingService; import org.springframework.util.StringUtils; /** @@ -304,7 +303,10 @@ public class DataBinderTests extends TestCase { public void testBindingWithFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); - binder.getFormatterRegistry().addFormatterByType(Float.class, new DecimalFormatter()); + GenericFormattingService formattingService = new GenericFormattingService(); + formattingService.setParentConversionService(new DefaultConversionService()); + formattingService.addFormatterForFieldType(Float.class, new DecimalFormatter()); + binder.setFormattingService(formattingService); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.addPropertyValue("myFloat", "1,2"); @@ -329,46 +331,6 @@ public class DataBinderTests extends TestCase { } } - public void testBindingWithDefaultFormatterFromField() { - doTestBindingWithDefaultFormatter(new FormattedFieldTestBean()); - } - - public void testBindingWithDefaultFormatterFromGetter() { - doTestBindingWithDefaultFormatter(new FormattedGetterTestBean()); - } - - public void testBindingWithDefaultFormatterFromSetter() { - doTestBindingWithDefaultFormatter(new FormattedSetterTestBean()); - } - - private void doTestBindingWithDefaultFormatter(Object tb) { - DataBinder binder = new DataBinder(tb); - // force formatter registry to be created - binder.getFormatterRegistry(); - MutablePropertyValues pvs = new MutablePropertyValues(); - pvs.addPropertyValue("number", "1,2"); - - LocaleContextHolder.setLocale(Locale.GERMAN); - try { - binder.bind(pvs); - assertEquals(new Float("1.2"), binder.getBindingResult().getRawFieldValue("number")); - assertEquals("1,2", binder.getBindingResult().getFieldValue("number")); - - PropertyEditor editor = binder.getBindingResult().findEditor("number", Float.class); - assertNotNull(editor); - editor.setValue(new Float("1.4")); - assertEquals("1,4", editor.getAsText()); - - editor = binder.getBindingResult().findEditor("number", null); - assertNotNull(editor); - editor.setAsText("1,6"); - assertEquals(new Float("1.6"), editor.getValue()); - } - finally { - LocaleContextHolder.resetLocaleContext(); - } - } - public void testBindingWithAllowedFields() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); @@ -1423,56 +1385,4 @@ public class DataBinderTests extends TestCase { } } - - @Target({ElementType.METHOD, ElementType.FIELD}) - @Retention(RetentionPolicy.RUNTIME) - @Formatted(DecimalFormatter.class) - public @interface Decimal { - } - - - private static class FormattedFieldTestBean { - - @Decimal - private Float number; - - public Float getNumber() { - return number; - } - - public void setNumber(Float number) { - this.number = number; - } - } - - - private static class FormattedGetterTestBean { - - private Float number; - - @Decimal - public Float getNumber() { - return number; - } - - public void setNumber(Float number) { - this.number = number; - } - } - - - private static class FormattedSetterTestBean { - - private Float number; - - public Float getNumber() { - return number; - } - - @Decimal - public void setNumber(Float number) { - this.number = number; - } - } - } diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java index 81697b66ec9..dba0a1300a4 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java @@ -16,8 +16,6 @@ package org.springframework.core.convert.support; -import java.util.Calendar; -import java.util.Date; import java.util.Locale; /** @@ -38,9 +36,6 @@ public class DefaultConversionService extends GenericConversionService { addConverter(String.class, Character.class, new StringToCharacterConverter()); addConverter(String.class, Locale.class, new StringToLocaleConverter()); addConverter(Number.class, Character.class, new NumberToCharacterConverter()); - addConverter(Date.class, Calendar.class, new DateToCalendarConverter()); - addConverter(Calendar.class, Date.class, new CalendarToDateConverter()); - JodaTimeConverters.addConverters(this); addConverter(Object.class, String.class, new ObjectToStringConverter()); addConverterFactory(String.class, Number.class, new StringToNumberConverterFactory()); addConverterFactory(String.class, Enum.class, new StringToEnumConverterFactory()); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java index e51f1a06a4c..09953aa0035 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -42,8 +42,6 @@ import org.springframework.util.ClassUtils; * @author Keith Donald * @author Juergen Hoeller * @since 3.0 - * @see #addConverter(Converter) - * @see #addConverterFactory(ConverterFactory) */ public class GenericConversionService implements ConversionService, ConverterRegistry { @@ -53,7 +51,7 @@ public class GenericConversionService implements ConversionService, ConverterReg } }; - private final Map> sourceTypeConverters = new HashMap>(36); + private final Map, Map, GenericConverter>> sourceTypeConverters = new HashMap, Map, GenericConverter>>(36); private ConversionService parent; @@ -90,8 +88,8 @@ public class GenericConversionService implements ConversionService, ConverterReg * JavaBean-friendly alternative to calling {@link #addConverter(Converter)}. * @see #addConverter(Converter) */ - public void setConverters(Set converters) { - for (Converter converter : converters) { + public void setConverters(Set> converters) { + for (Converter converter : converters) { addConverter(converter); } } @@ -101,8 +99,8 @@ public class GenericConversionService implements ConversionService, ConverterReg * JavaBean-friendly alternative to calling {@link #addConverterFactory(ConverterFactory)}. * @see #addConverterFactory(ConverterFactory) */ - public void setConverterFactories(Set converters) { - for (ConverterFactory converterFactory : converters) { + public void setConverterFactories(Set> converters) { + for (ConverterFactory converterFactory : converters) { addConverterFactory(converterFactory); } } @@ -124,24 +122,24 @@ public class GenericConversionService implements ConversionService, ConverterReg // implementing ConverterRegistry public void addConverter(Converter converter) { - Class[] typeInfo = getRequiredTypeInfo(converter, Converter.class); + Class[] typeInfo = getRequiredTypeInfo(converter, Converter.class); if (typeInfo == null) { throw new IllegalArgumentException( "Unable to the determine sourceType and targetType your Converter converts between; declare these types or implement ConverterInfo"); } - Class sourceType = typeInfo[0]; - Class targetType = typeInfo[1]; + Class sourceType = typeInfo[0]; + Class targetType = typeInfo[1]; addConverter(sourceType, targetType, converter); } public void addConverterFactory(ConverterFactory converterFactory) { - Class[] typeInfo = getRequiredTypeInfo(converterFactory, ConverterFactory.class); + Class[] typeInfo = getRequiredTypeInfo(converterFactory, ConverterFactory.class); if (typeInfo == null) { throw new IllegalArgumentException( "Unable to the determine sourceType and targetRangeType R your ConverterFactory converts between; declare these types or implement ConverterInfo"); } - Class sourceType = typeInfo[0]; - Class targetType = typeInfo[1]; + Class sourceType = typeInfo[0]; + Class targetType = typeInfo[1]; addConverterFactory(sourceType, targetType, converterFactory); } @@ -155,6 +153,7 @@ public class GenericConversionService implements ConversionService, ConverterReg return canConvert(TypeDescriptor.valueOf(sourceType), TypeDescriptor.valueOf(targetType)); } + @SuppressWarnings("unchecked") public T convert(Object source, Class targetType) { return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType)); } @@ -279,46 +278,46 @@ public class GenericConversionService implements ConversionService, ConverterReg Assert.notNull(targetType, "The targetType to convert to is required"); } - private Class[] getRequiredTypeInfo(Object converter, Class genericIfc) { + private Class[] getRequiredTypeInfo(Object converter, Class genericIfc) { return GenericTypeResolver.resolveTypeArguments(converter.getClass(), genericIfc); } - private GenericConverter findConverterByClassPair(Class sourceType, Class targetType) { + private GenericConverter findConverterByClassPair(Class sourceType, Class targetType) { if (sourceType.isInterface()) { - LinkedList classQueue = new LinkedList(); + LinkedList> classQueue = new LinkedList>(); classQueue.addFirst(sourceType); while (!classQueue.isEmpty()) { - Class currentClass = classQueue.removeLast(); - Map converters = getConvertersForSource(currentClass); + Class currentClass = classQueue.removeLast(); + Map, GenericConverter> converters = getConvertersForSource(currentClass); GenericConverter converter = getConverter(converters, targetType); if (converter != null) { return converter; } - Class[] interfaces = currentClass.getInterfaces(); - for (Class ifc : interfaces) { + Class[] interfaces = currentClass.getInterfaces(); + for (Class ifc : interfaces) { classQueue.addFirst(ifc); } } - Map objectConverters = getConvertersForSource(Object.class); + Map, GenericConverter> objectConverters = getConvertersForSource(Object.class); return getConverter(objectConverters, targetType); } else { - LinkedList classQueue = new LinkedList(); + LinkedList> classQueue = new LinkedList>(); classQueue.addFirst(sourceType); while (!classQueue.isEmpty()) { - Class currentClass = classQueue.removeLast(); - Map converters = getConvertersForSource(currentClass); + Class currentClass = classQueue.removeLast(); + Map, GenericConverter> converters = getConvertersForSource(currentClass); GenericConverter converter = getConverter(converters, targetType); if (converter != null) { return converter; } if (currentClass.isArray()) { - Class componentType = ClassUtils.resolvePrimitiveIfNecessary(currentClass.getComponentType()); + Class componentType = ClassUtils.resolvePrimitiveIfNecessary(currentClass.getComponentType()); if (componentType.getSuperclass() != null) { classQueue.addFirst(Array.newInstance(componentType.getSuperclass(), 0).getClass()); } } else { - Class[] interfaces = currentClass.getInterfaces(); - for (Class ifc : interfaces) { + Class[] interfaces = currentClass.getInterfaces(); + for (Class ifc : interfaces) { classQueue.addFirst(ifc); } if (currentClass.getSuperclass() != null) { @@ -330,56 +329,56 @@ public class GenericConversionService implements ConversionService, ConverterReg } } - private Map getSourceMap(Class sourceType) { - Map sourceMap = sourceTypeConverters.get(sourceType); + private Map, GenericConverter> getSourceMap(Class sourceType) { + Map, GenericConverter> sourceMap = sourceTypeConverters.get(sourceType); if (sourceMap == null) { - sourceMap = new HashMap(); + sourceMap = new HashMap, GenericConverter>(); this.sourceTypeConverters.put(sourceType, sourceMap); } return sourceMap; } - private Map getConvertersForSource(Class sourceType) { - Map converters = this.sourceTypeConverters.get(sourceType); + private Map, GenericConverter> getConvertersForSource(Class sourceType) { + Map, GenericConverter> converters = this.sourceTypeConverters.get(sourceType); if (converters == null) { converters = Collections.emptyMap(); } return converters; } - private GenericConverter getConverter(Map converters, Class targetType) { + private GenericConverter getConverter(Map, GenericConverter> converters, Class targetType) { if (targetType.isInterface()) { - LinkedList classQueue = new LinkedList(); + LinkedList> classQueue = new LinkedList>(); classQueue.addFirst(targetType); while (!classQueue.isEmpty()) { - Class currentClass = classQueue.removeLast(); + Class currentClass = classQueue.removeLast(); GenericConverter converter = converters.get(currentClass); if (converter != null) { return converter; } - Class[] interfaces = currentClass.getInterfaces(); - for (Class ifc : interfaces) { + Class[] interfaces = currentClass.getInterfaces(); + for (Class ifc : interfaces) { classQueue.addFirst(ifc); } } return converters.get(Object.class); } else { - LinkedList classQueue = new LinkedList(); + LinkedList> classQueue = new LinkedList>(); classQueue.addFirst(targetType); while (!classQueue.isEmpty()) { - Class currentClass = classQueue.removeLast(); + Class currentClass = classQueue.removeLast(); GenericConverter converter = converters.get(currentClass); if (converter != null) { return converter; } if (currentClass.isArray()) { - Class componentType = ClassUtils.resolvePrimitiveIfNecessary(currentClass.getComponentType()); + Class componentType = ClassUtils.resolvePrimitiveIfNecessary(currentClass.getComponentType()); if (componentType.getSuperclass() != null) { classQueue.addFirst(Array.newInstance(componentType.getSuperclass(), 0).getClass()); } } else { - Class[] interfaces = currentClass.getInterfaces(); - for (Class ifc : interfaces) { + Class[] interfaces = currentClass.getInterfaces(); + for (Class ifc : interfaces) { classQueue.addFirst(ifc); } if (currentClass.getSuperclass() != null) { @@ -391,11 +390,12 @@ public class GenericConversionService implements ConversionService, ConverterReg } } + @SuppressWarnings("unchecked") private final class ConverterAdapter implements GenericConverter { private final Converter converter; - public ConverterAdapter(Converter converter) { + public ConverterAdapter(Converter converter) { this.converter = converter; } @@ -407,11 +407,12 @@ public class GenericConversionService implements ConversionService, ConverterReg } } + @SuppressWarnings("unchecked") private final class ConverterFactoryAdapter implements GenericConverter { private final ConverterFactory converterFactory; - public ConverterFactoryAdapter(ConverterFactory converterFactory) { + public ConverterFactoryAdapter(ConverterFactory converterFactory) { this.converterFactory = converterFactory; } diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/JodaTimeConverters.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/JodaTimeConverters.java deleted file mode 100644 index 6976cb29eae..00000000000 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/JodaTimeConverters.java +++ /dev/null @@ -1,130 +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.core.convert.support; - -import java.util.Calendar; -import java.util.Date; - -import org.joda.time.DateMidnight; -import org.joda.time.DateTime; -import org.joda.time.LocalDate; -import org.joda.time.LocalDateTime; -import org.joda.time.LocalTime; -import org.springframework.core.convert.converter.Converter; -import org.springframework.util.ClassUtils; - -class JodaTimeConverters { - - public static void addConverters(GenericConversionService registry) { - if (!isJodaTimePresent()) { - return; - } - registry.addConverter(DateTime.class, LocalDate.class, new DateTimeToLocalDateConverter()); - registry.addConverter(LocalDate.class, DateTime.class, new LocalDateToDateTimeConverter()); - - registry.addConverter(DateTime.class, LocalTime.class, new DateTimeToLocalTimeConverter()); - registry.addConverter(LocalTime.class, DateTime.class, new LocalTimeToDateTimeConverter()); - - registry.addConverter(DateTime.class, LocalDateTime.class, new DateTimeToLocalDateTimeConverter()); - registry.addConverter(LocalDateTime.class, DateTime.class, new LocalDateTimeToDateTimeConverter()); - - registry.addConverter(DateTime.class, DateMidnight.class, new DateTimeToDateMidnightConverter()); - registry.addConverter(DateMidnight.class, Date.class, new DateMidnightToDateTimeConverter()); - - registry.addConverter(DateTime.class, Date.class, new DateTimeToDateConverter()); - registry.addConverter(Date.class, DateTime.class, new DateToDateTimeConverter()); - - registry.addConverter(DateTime.class, Calendar.class, new DateTimeToCalendarConverter()); - registry.addConverter(Calendar.class, DateTime.class, new CalendarToDateTimeConverter()); - } - - private static boolean isJodaTimePresent() { - return ClassUtils.isPresent("org.joda.time.DateTime", JodaTimeConverters.class.getClassLoader()); - } - - private static class DateTimeToLocalDateConverter implements Converter { - public LocalDate convert(DateTime source) { - return source.toLocalDate(); - } - } - - private static class LocalDateToDateTimeConverter implements Converter { - public DateTime convert(LocalDate source) { - return source.toDateTimeAtStartOfDay(); - } - } - - private static class DateTimeToLocalTimeConverter implements Converter { - public LocalTime convert(DateTime source) { - return source.toLocalTime(); - } - } - - private static class LocalTimeToDateTimeConverter implements Converter { - public DateTime convert(LocalTime source) { - return source.toDateTimeToday(); - } - } - - private static class DateTimeToLocalDateTimeConverter implements Converter { - public LocalDateTime convert(DateTime source) { - return source.toLocalDateTime(); - } - } - - private static class LocalDateTimeToDateTimeConverter implements Converter { - public DateTime convert(LocalDateTime source) { - return source.toDateTime(); - } - } - - private static class DateTimeToDateMidnightConverter implements Converter { - public DateMidnight convert(DateTime source) { - return source.toDateMidnight(); - } - } - - private static class DateMidnightToDateTimeConverter implements Converter { - public DateTime convert(DateMidnight source) { - return source.toDateTime(); - } - } - - private static class DateTimeToDateConverter implements Converter { - public Date convert(DateTime source) { - return source.toDate(); - } - } - - private static class DateToDateTimeConverter implements Converter { - public DateTime convert(Date source) { - return new DateTime(source); - } - } - - private static class DateTimeToCalendarConverter implements Converter { - public Calendar convert(DateTime source) { - return source.toGregorianCalendar(); - } - } - - private static class CalendarToDateTimeConverter implements Converter { - public DateTime convert(Calendar source) { - return new DateTime(source); - } - } - -} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java index 38d878ca03c..deb338fcfdc 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java @@ -77,8 +77,6 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; -import org.springframework.ui.format.date.DateFormatter; -import org.springframework.ui.format.support.GenericFormatterRegistry; import org.springframework.util.SerializationTestUtils; import org.springframework.util.StringUtils; import org.springframework.util.MultiValueMap; @@ -487,37 +485,6 @@ public class ServletAnnotationControllerTests { assertEquals("myView-String:myDefaultName-typeMismatch-tb1-myOriginalValue", response.getContentAsString()); } - @Test - public void commandProvidingFormControllerWithFormatter() throws Exception { - @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { - @Override - protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { - GenericWebApplicationContext wac = new GenericWebApplicationContext(); - wac.registerBeanDefinition("controller", new RootBeanDefinition(MyCommandProvidingFormController.class)); - wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)); - RootBeanDefinition registryDef = new RootBeanDefinition(GenericFormatterRegistry.class); - registryDef.getPropertyValues().addPropertyValue("formatters", new DateFormatter("yyyy-MM-dd")); - RootBeanDefinition initializerDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class); - initializerDef.getPropertyValues().addPropertyValue("formatterRegistry", registryDef); - initializerDef.getPropertyValues().addPropertyValue("validator", new RootBeanDefinition(LocalValidatorFactoryBean.class)); - RootBeanDefinition adapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); - adapterDef.getPropertyValues().addPropertyValue("webBindingInitializer", initializerDef); - wac.registerBeanDefinition("handlerAdapter", adapterDef); - wac.refresh(); - return wac; - } - }; - servlet.init(new MockServletConfig()); - - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); - request.addParameter("defaultName", "myDefaultName"); - request.addParameter("age", "value2"); - request.addParameter("date", "2007-10-02"); - MockHttpServletResponse response = new MockHttpServletResponse(); - servlet.service(request, response); - assertEquals("myView-String:myDefaultName-typeMismatch-tb1-myOriginalValue", response.getContentAsString()); - } - @Test public void typedCommandProvidingFormController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java index d0cb13e3df2..70af4f5f96e 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java @@ -17,7 +17,7 @@ package org.springframework.web.bind.support; import org.springframework.beans.PropertyEditorRegistrar; -import org.springframework.ui.format.FormatterRegistry; +import org.springframework.ui.format.FormattingService; import org.springframework.validation.BindingErrorProcessor; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; @@ -46,7 +46,7 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer private Validator validator; - private FormatterRegistry formatterRegistry; + private FormattingService formattingService; private PropertyEditorRegistrar[] propertyEditorRegistrars; @@ -111,17 +111,17 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer } /** - * Specify a FormatterRegistry which will apply to every DataBinder. + * Specify a FormattingService which will apply to every DataBinder. */ - public final void setFormatterRegistry(FormatterRegistry formatterRegistry) { - this.formatterRegistry = formatterRegistry; + public final void setFormattingService(FormattingService formattingService) { + this.formattingService = formattingService; } /** - * Return a FormatterRegistry which will apply to every DataBinder. + * Return the FormattingService which will apply to every DataBinder. */ - public final FormatterRegistry getFormatterRegistry() { - return this.formatterRegistry; + public final FormattingService getFormattingService() { + return this.formattingService; } /** @@ -160,8 +160,8 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer this.validator.supports(binder.getTarget().getClass())) { binder.setValidator(this.validator); } - if (this.formatterRegistry != null) { - binder.setFormatterRegistry(this.formatterRegistry); + if (this.formattingService != null) { + binder.setFormattingService(this.formattingService); } if (this.propertyEditorRegistrars != null) { for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) {