diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/BeanTypeDescriptor.java b/org.springframework.beans/src/main/java/org/springframework/beans/BeanTypeDescriptor.java new file mode 100644 index 00000000000..06742155f5e --- /dev/null +++ b/org.springframework.beans/src/main/java/org/springframework/beans/BeanTypeDescriptor.java @@ -0,0 +1,90 @@ +/* + * 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.beans; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.util.ReflectionUtils; + +/** + * {@link TypeDescriptor} extension that exposes additional annotations + * as conversion metadata: namely, annotations on other accessor methods + * (getter/setter) and on the underlying field, if found. + * + * @author Juergen Hoeller + * @since 3.0 + */ +class BeanTypeDescriptor extends TypeDescriptor { + + private final PropertyDescriptor propertyDescriptor; + + private Annotation[] cachedAnnotations; + + + /** + * Create a new BeanTypeDescriptor for the given bean property. + * @param methodParameter the target method parameter + * @param propertyDescriptor the corresponding JavaBean PropertyDescriptor + */ + public BeanTypeDescriptor(MethodParameter methodParameter, PropertyDescriptor propertyDescriptor) { + super(methodParameter); + this.propertyDescriptor = propertyDescriptor; + } + + + @Override + public Annotation[] getAnnotations() { + Annotation[] anns = this.cachedAnnotations; + if (anns == null) { + Field underlyingField = ReflectionUtils.findField( + getMethodParameter().getMethod().getDeclaringClass(), this.propertyDescriptor.getName()); + Map annMap = new LinkedHashMap(); + if (underlyingField != null) { + for (Annotation ann : underlyingField.getAnnotations()) { + annMap.put(ann.annotationType(), ann); + } + } + Method targetMethod = getMethodParameter().getMethod(); + Method writeMethod = this.propertyDescriptor.getWriteMethod(); + Method readMethod = this.propertyDescriptor.getReadMethod(); + if (writeMethod != null && writeMethod != targetMethod) { + for (Annotation ann : writeMethod.getAnnotations()) { + annMap.put(ann.annotationType(), ann); + } + } + if (readMethod != null && readMethod != targetMethod) { + for (Annotation ann : readMethod.getAnnotations()) { + annMap.put(ann.annotationType(), ann); + } + } + for (Annotation ann : targetMethod.getAnnotations()) { + annMap.put(ann.annotationType(), ann); + } + anns = annMap.values().toArray(new Annotation[annMap.size()]); + this.cachedAnnotations = anns; + } + return anns; + } + +} diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index 86f92deaffc..1fc2823f0b8 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -333,10 +333,10 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName); if (pd != null) { if (pd.getReadMethod() != null) { - return new TypeDescriptor(new MethodParameter(pd.getReadMethod(), -1)); + return new BeanTypeDescriptor(new MethodParameter(pd.getReadMethod(), -1), pd); } else if (pd.getWriteMethod() != null) { - return new TypeDescriptor(new MethodParameter(pd.getWriteMethod(), 0)); + return new BeanTypeDescriptor(BeanUtils.getWriteMethodParameter(pd), pd); } } } @@ -947,7 +947,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra writeMethod.invoke(object, value); return null; } - },acc); + }, acc); } catch (PrivilegedActionException ex) { throw ex.getException(); } diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/PropertyAccessor.java b/org.springframework.beans/src/main/java/org/springframework/beans/PropertyAccessor.java index e9a980c6ddb..d9f03be6b8c 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/PropertyAccessor.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/PropertyAccessor.java @@ -89,7 +89,8 @@ public interface PropertyAccessor { Class getPropertyType(String propertyName) throws BeansException; /** - * Return a type descriptor for the specified property. + * Return a type descriptor for the specified property: + * preferably from the read method, falling back to the write method. * @param propertyName the property to check * (may be a nested path and/or an indexed/mapped property) * @return the property type for the particular property, diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java b/org.springframework.beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java index 2ad4877376b..79d135c5484 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java @@ -165,13 +165,17 @@ class TypeConverterDelegate { // No custom editor but custom ConversionService specified? ConversionService conversionService = this.propertyEditorRegistry.getConversionService(); - if (editor == null && conversionService != null && convertedValue != null && - conversionService.canConvert(convertedValue.getClass(), requiredType)) { + if (editor == null && conversionService != null && convertedValue != null) { + TypeDescriptor typeDesc; if (methodParam != null) { - return (T) conversionService.convert(convertedValue, new TypeDescriptor(methodParam)); + typeDesc = (descriptor != null ? + new BeanTypeDescriptor(methodParam, descriptor) : new TypeDescriptor(methodParam)); } else { - return conversionService.convert(convertedValue, requiredType); + typeDesc = TypeDescriptor.valueOf(requiredType); + } + if (conversionService.canConvert(convertedValue.getClass(), typeDesc)) { + return (T) conversionService.convert(convertedValue, typeDesc); } } @@ -353,21 +357,26 @@ class TypeConverterDelegate { convertedValue = StringUtils.arrayToCommaDelimitedString((String[]) convertedValue); } - if (editor != null && convertedValue instanceof String) { - // Use PropertyEditor's setAsText in case of a String value. - if (logger.isTraceEnabled()) { - logger.trace("Converting String to [" + requiredType + "] using property editor [" + editor + "]"); - } - String newTextValue = (String) convertedValue; - if (sharedEditor) { - // Synchronized access to shared editor instance. - synchronized (editor) { + if (convertedValue instanceof String) { + if (editor != null) { + // Use PropertyEditor's setAsText in case of a String value. + if (logger.isTraceEnabled()) { + logger.trace("Converting String to [" + requiredType + "] using property editor [" + editor + "]"); + } + String newTextValue = (String) convertedValue; + if (sharedEditor) { + // Synchronized access to shared editor instance. + synchronized (editor) { + return doConvertTextValue(oldValue, newTextValue, editor); + } + } + else { + // Unsynchronized access to non-shared editor instance. return doConvertTextValue(oldValue, newTextValue, editor); } } - else { - // Unsynchronized access to non-shared editor instance. - return doConvertTextValue(oldValue, newTextValue, editor); + else if (String.class.equals(requiredType)) { + returnValue = convertedValue; } } diff --git a/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index a9a2885613e..d28bfd04981 100644 --- a/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -832,7 +832,7 @@ public final class DefaultListableBeanFactoryTests { public void testCustomConverter() { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); DefaultConversionService conversionService = new DefaultConversionService(); - conversionService.add(new Converter() { + conversionService.addConverter(new Converter() { public Float convert(String source) throws Exception { NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN); return nf.parse(source).floatValue(); 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 23e4885ddad..9b74add6302 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 @@ -27,8 +27,8 @@ import java.lang.annotation.Annotation; * * @author Keith Donald * @since 3.0 - * @param The type of Annotation this factory uses to create Formatter instances - * @param The type of Object Formatters created by this factory format + * @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 */ public interface AnnotationFormatterFactory { 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/Formatted.java index 82370ae9580..41906262094 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/Formatted.java @@ -36,6 +36,6 @@ public @interface Formatted { /** * The Formatter that handles the formatting for the annotated element. */ - Class value(); + Class value(); } 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 7dffdf9a287..7c27339618e 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 @@ -27,7 +27,7 @@ import java.util.Locale; * @param the type of object this formatter can format */ public interface Formatter { - + /** * Format the object of type T for display. * @param object the object to format @@ -35,13 +35,15 @@ public interface Formatter { * @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; 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 d9d5a7e63b3..a5ec12b974a 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 @@ -28,30 +28,52 @@ import org.springframework.core.convert.TypeDescriptor; */ public interface FormatterRegistry { - /** - * Adds a Formatter to this registry indexed by <T>. - * Calling getFormatter(<T>.class) returns formatter. - * @param formatter the formatter - * @param the type of object the formatter formats - */ - void add(Formatter formatter); - /** * 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. - * On format, the decorator first coerses 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 coerses the parsed value to 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 coerses 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 */ - void add(Class type, Formatter targetFormatter); + void addFormatterByType(Class type, Formatter targetFormatter); + + /** + * Adds a Formatter to this registry indexed by <T>. + * Calling getFormatter(<T>.class) returns formatter. + * @param formatter the formatter + * @param the type of object the formatter formats + */ + void addFormatterByType(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 + * @param the type of object the formatter formats + */ + void addFormatterByAnnotation(Class annotationType, Formatter formatter); /** * 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 + * @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 */ - void add(AnnotationFormatterFactory factory); + void addFormatterByAnnotation(AnnotationFormatterFactory factory); + + /** + * Get the Formatter for the specified type. + * @return the Formatter, or null if no suitable one is registered + */ + Formatter getFormatter(Class targetType); /** * Get the Formatter for the type descriptor. 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 296dfd2febf..e958e35e599 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 @@ -21,6 +21,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import java.util.TimeZone; import org.springframework.ui.format.Formatter; @@ -33,12 +34,16 @@ import org.springframework.ui.format.Formatter; * @since 3.0 * @see SimpleDateFormat */ -public final class DateFormatter implements Formatter { +public class DateFormatter implements Formatter { private String pattern; private int style = DateFormat.DEFAULT; + private TimeZone timeZone; + + private boolean lenient = false; + /** * Create a new default DateFormatter. @@ -75,6 +80,22 @@ public final class DateFormatter implements Formatter { this.style = style; } + /** + * Set the TimeZone to normalize the date values into, if any. + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Specify whether or not parsing is to be lenient. Default is false. + *

With lenient parsing, the parser may allow inputs that do not precisely match the format. + * With strict parsing, inputs must match the format exactly. + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + public String format(Date date, Locale locale) { if (date == null) { @@ -99,7 +120,10 @@ public final class DateFormatter implements Formatter { else { dateFormat = DateFormat.getDateInstance(this.style, locale); } - dateFormat.setLenient(false); + if (this.timeZone != null) { + dateFormat.setTimeZone(this.timeZone); + } + dateFormat.setLenient(this.lenient); return dateFormat; } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/date/package-info.java b/org.springframework.context/src/main/java/org/springframework/ui/format/date/package-info.java index 60f5e723638..fb9cb1fe70d 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/date/package-info.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/date/package-info.java @@ -1,5 +1,4 @@ /** - * Formatters for java.util.Date fields. + * Formatters for java.util.Date properties. */ package org.springframework.ui.format.date; - 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 new file mode 100644 index 00000000000..5dc4b1c0c85 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/AbstractNumberFormatter.java @@ -0,0 +1,83 @@ +/* + * 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.number; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.Locale; + +import org.springframework.ui.format.Formatter; + +/** + * Abstract formatter for Numbers, + * providing a {@link #getNumberFormat(java.util.Locale)} template method. + * + * @author Juergen Hoeller + * @author Keith Donald + * @since 3.0 + */ +public abstract class AbstractNumberFormatter implements Formatter { + + private boolean lenient = false; + + + /** + * Specify whether or not parsing is to be lenient. Default is false. + *

With lenient parsing, the parser may allow inputs that do not precisely match the format. + * With strict parsing, inputs must match the format exactly. + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + + public String format(Number integer, Locale locale) { + if (integer == null) { + return ""; + } + NumberFormat format = getNumberFormat(locale); + return format.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); + if (position.getErrorIndex() != -1) { + throw new ParseException(formatted, position.getIndex()); + } + if (!this.lenient) { + if (formatted.length() != position.getIndex()) { + // indicates a part of the string that was not parsed + throw new ParseException(formatted, position.getIndex()); + } + } + return number; + } + + /** + * Obtain a concrete NumberFormat for the specified locale. + * @param locale the current locale + * @return the NumberFormat instance (never null) + */ + protected abstract NumberFormat getNumberFormat(Locale locale); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java index 23807b78184..edfb4adae3a 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java @@ -1,82 +1,103 @@ /* - * Copyright 2004-2009 the original author or authors. - * + * 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.number; import java.math.BigDecimal; import java.math.RoundingMode; +import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; -import java.text.ParsePosition; +import java.util.Currency; import java.util.Locale; -import org.springframework.ui.format.Formatter; +import org.springframework.util.ClassUtils; /** * A BigDecimal formatter for currency values. - * Delegates to {@link NumberFormat#getCurrencyInstance(Locale)}. + * + *

Delegates to {@link NumberFormat#getCurrencyInstance(Locale)}. * Configures BigDecimal parsing so there is no loss of precision. - * Sets the scale of parsed BigDecimal values to {@link NumberFormat#getMaximumFractionDigits()}. - * Applies {@link RoundingMode#DOWN} to parsed values. + * Can apply a specified {@link RoundingMode} to parsed values. + * * @author Keith Donald + * @author Juergen Hoeller * @since 3.0 - * @see #setLenient(boolean) + * @see #setLenient + * @see #setRoundingMode */ -public final class CurrencyFormatter implements Formatter { +public final class CurrencyFormatter extends AbstractNumberFormatter { - private CurrencyNumberFormatFactory currencyFormatFactory = new CurrencyNumberFormatFactory(); + private static final boolean roundingModeOnDecimalFormat = + ClassUtils.hasMethod(DecimalFormat.class, "setRoundingMode", RoundingMode.class); + + private int fractionDigits = 2; + + private RoundingMode roundingMode; + + private Currency currency; - private boolean lenient; /** - * Specify whether or not parsing is to be lenient. - * With lenient parsing, the parser may allow inputs that do not precisely match the format. - * With strict parsing, inputs must match the format exactly. - * Default is false. + * Specify the desired number of fraction digits. + * Default is 2. */ - public void setLenient(boolean lenient) { - this.lenient = lenient; + public void setFractionDigits(int fractionDigits) { + this.fractionDigits = fractionDigits; } - public String format(BigDecimal decimal, Locale locale) { - if (decimal == null) { - return ""; - } - NumberFormat format = currencyFormatFactory.getNumberFormat(locale); - return format.format(decimal); + /** + * Specify the rounding mode to use for decimal parsing. + * Default is {@link RoundingMode#UNNECESSARY}. + */ + public void setRoundingMode(RoundingMode roundingMode) { + this.roundingMode = roundingMode; } - public BigDecimal parse(String formatted, Locale locale) - throws ParseException { - if (formatted.length() == 0) { - return null; - } - NumberFormat format = currencyFormatFactory.getNumberFormat(locale); - ParsePosition position = new ParsePosition(0); - BigDecimal decimal = (BigDecimal) format.parse(formatted, position); - if (position.getErrorIndex() != -1) { - throw new ParseException(formatted, position.getIndex()); + /** + * Specify the currency, if known. + */ + public void setCurrency(Currency currency) { + this.currency = currency; + } + + + public BigDecimal parse(String formatted, Locale locale) throws ParseException { + BigDecimal decimal = (BigDecimal) super.parse(formatted, locale); + if (this.roundingMode != null) { + decimal = decimal.setScale(this.fractionDigits, this.roundingMode); + } + else { + decimal = decimal.setScale(this.fractionDigits); } - if (!lenient) { - if (formatted.length() != position.getIndex()) { - // indicates a part of the string that was not parsed - throw new ParseException(formatted, position.getIndex()); - } - } - decimal = decimal.setScale(format.getMaximumFractionDigits(), format.getRoundingMode()); return decimal; } - -} \ No newline at end of file + + protected NumberFormat getNumberFormat(Locale locale) { + DecimalFormat format = (DecimalFormat) NumberFormat.getCurrencyInstance(locale); + format.setParseBigDecimal(true); + format.setMaximumFractionDigits(this.fractionDigits); + format.setMinimumFractionDigits(this.fractionDigits); + if (this.roundingMode != null && roundingModeOnDecimalFormat) { + format.setRoundingMode(this.roundingMode); + } + if (this.currency != null) { + format.setCurrency(this.currency); + } + return format; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyNumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyNumberFormatFactory.java deleted file mode 100644 index 648feb4ae0c..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyNumberFormatFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2004-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.number; - -import java.math.RoundingMode; -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.util.Locale; - -/** - * Produces NumberFormat instances that format currency values. - * @author Keith Donald - * @since 3.0 - * @see NumberFormat - */ -final class CurrencyNumberFormatFactory extends NumberFormatFactory { - - private RoundingMode roundingMode = RoundingMode.DOWN; - - public NumberFormat getNumberFormat(Locale locale) { - DecimalFormat format = (DecimalFormat) NumberFormat.getCurrencyInstance(locale); - format.setParseBigDecimal(true); - format.setRoundingMode(roundingMode); - return format; - } -} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java index 493b6017cf0..2672d57d4d2 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java @@ -1,49 +1,43 @@ /* - * Copyright 2004-2009 the original author or authors. - * + * 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.number; -import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.NumberFormat; -import java.text.ParseException; -import java.text.ParsePosition; import java.util.Locale; -import org.springframework.ui.format.Formatter; - /** * A Number formatter for decimal values. - * Delegates to {@link NumberFormat#getInstance(Locale)}. + * + *

Delegates to {@link NumberFormat#getInstance(Locale)}. * Configures BigDecimal parsing so there is no loss in precision. * Allows configuration over the decimal number pattern. * The {@link #parse(String, Locale)} routine always returns a BigDecimal. + * * @author Keith Donald + * @author Juergen Hoeller * @since 3.0 - * @see #setPattern(String) - * @see #setLenient(boolean) + * @see #setPattern + * @see #setLenient */ -public final class DecimalFormatter implements Formatter { +public final class DecimalFormatter extends AbstractNumberFormatter { - private DefaultNumberFormatFactory formatFactory = new DefaultNumberFormatFactory(); + private String pattern; - private boolean lenient; - - public DecimalFormatter() { - initDefaults(); - } /** * Sets the pattern to use to format number values. @@ -52,50 +46,24 @@ public final class DecimalFormatter implements Formatter { * @see DecimalFormat#applyPattern(String) */ public void setPattern(String pattern) { - formatFactory.setPattern(pattern); + this.pattern = pattern; } - /** - * Specify whether or not parsing is to be lenient. - * With lenient parsing, the parser may allow inputs that do not precisely match the format. - * With strict parsing, inputs must match the format exactly. - * Default is false. - */ - public void setLenient(boolean lenient) { - this.lenient = lenient; - } - public String format(Number decimal, Locale locale) { - if (decimal == null) { - return ""; - } - NumberFormat format = formatFactory.getNumberFormat(locale); - return format.format(decimal); - } - - public Number parse(String formatted, Locale locale) throws ParseException { - if (formatted.length() == 0) { - return null; - } - NumberFormat format = formatFactory.getNumberFormat(locale); - ParsePosition position = new ParsePosition(0); - BigDecimal decimal = (BigDecimal) format.parse(formatted, position); - if (position.getErrorIndex() != -1) { - throw new ParseException(formatted, position.getIndex()); - } - if (!lenient) { - if (formatted.length() != position.getIndex()) { - // indicates a part of the string that was not parsed - throw new ParseException(formatted, position.getIndex()); + public NumberFormat getNumberFormat(Locale locale) { + NumberFormat format = NumberFormat.getInstance(locale); + if (!(format instanceof DecimalFormat)) { + if (this.pattern != null) { + throw new IllegalStateException("Cannot support pattern for non-DecimalFormat: " + format); } + return format; } - return decimal; + DecimalFormat decimalFormat = (DecimalFormat) format; + decimalFormat.setParseBigDecimal(true); + if (this.pattern != null) { + decimalFormat.applyPattern(this.pattern); + } + return decimalFormat; } - // internal helpers - - private void initDefaults() { - formatFactory.setParseBigDecimal(true); - } - -} \ No newline at end of file +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/DefaultNumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/DefaultNumberFormatFactory.java deleted file mode 100644 index 1cdd51262c8..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/DefaultNumberFormatFactory.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2004-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.number; - -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.util.Locale; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * Works with a general purpose {@link DecimalFormat} instance returned by calling - * {@link NumberFormat#getInstance(Locale)} by default. - * @author Keith Donald - * @see NumberFormat - * @see DecimalFormat - * @since 3.0 - */ -class DefaultNumberFormatFactory extends NumberFormatFactory { - - private static Log logger = LogFactory.getLog(DefaultNumberFormatFactory.class); - - private String pattern; - - private Boolean parseBigDecimal; - - /** - * Sets the pattern to use to format number values. - * If not specified, the default DecimalFormat pattern is used. - * @param pattern the format pattern - * @see DecimalFormat#applyPattern(String) - */ - public void setPattern(String pattern) { - this.pattern = pattern; - } - - /** - * Sets whether the format should always parse a big decimal. - * @param parseBigDecimal the big decimal parse status - * @see DecimalFormat#setParseBigDecimal(boolean) - */ - public void setParseBigDecimal(boolean parseBigDecimal) { - this.parseBigDecimal = parseBigDecimal; - } - - public NumberFormat getNumberFormat(Locale locale) { - NumberFormat format = NumberFormat.getInstance(locale); - if (pattern != null) { - if (format instanceof DecimalFormat) { - ((DecimalFormat) format).applyPattern(pattern); - } else { - logger.warn("Unable to apply format pattern '" + pattern - + "'; Returned NumberFormat is not a DecimalFormat"); - } - } - if (parseBigDecimal != null) { - if (format instanceof DecimalFormat) { - ((DecimalFormat) format).setParseBigDecimal(parseBigDecimal); - } else { - logger.warn("Unable to call setParseBigDecimal; not a DecimalFormat"); - } - } - return format; - } - -} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java index 1fc85b8e084..5cda7225a2b 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java @@ -1,77 +1,39 @@ /* - * Copyright 2004-2009 the original author or authors. - * + * 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.number; import java.text.NumberFormat; -import java.text.ParseException; -import java.text.ParsePosition; import java.util.Locale; -import org.springframework.ui.format.Formatter; - /** * A Number formatter for whole integer values. - * Delegates to {@link NumberFormat#getIntegerInstance(Locale)}. + * + *

Delegates to {@link NumberFormat#getIntegerInstance(Locale)}. * The {@link #parse(String, Locale)} routine always returns a Long. + * * @author Keith Donald + * @author Juergen Hoeller * @since 3.0 - * @see #setLenient(boolean) + * @see #setLenient */ -public final class IntegerFormatter implements Formatter { +public final class IntegerFormatter extends AbstractNumberFormatter { - private IntegerNumberFormatFactory formatFactory = new IntegerNumberFormatFactory(); - - private boolean lenient; - - /** - * Specify whether or not parsing is to be lenient. - * With lenient parsing, the parser may allow inputs that do not precisely match the format. - * With strict parsing, inputs must match the format exactly. - * Default is false. - */ - public void setLenient(boolean lenient) { - this.lenient = lenient; + protected NumberFormat getNumberFormat(Locale locale) { + return NumberFormat.getIntegerInstance(locale); } - public String format(Number integer, Locale locale) { - if (integer == null) { - return ""; - } - NumberFormat format = formatFactory.getNumberFormat(locale); - return format.format(integer); - } - - public Number parse(String formatted, Locale locale) - throws ParseException { - if (formatted.length() == 0) { - return null; - } - NumberFormat format = formatFactory.getNumberFormat(locale); - ParsePosition position = new ParsePosition(0); - Long integer = (Long) format.parse(formatted, position); - if (position.getErrorIndex() != -1) { - throw new ParseException(formatted, position.getIndex()); - } - if (!lenient) { - if (formatted.length() != position.getIndex()) { - // indicates a part of the string that was not parsed - throw new ParseException(formatted, position.getIndex()); - } - } - return integer; - } - -} \ No newline at end of file +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerNumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerNumberFormatFactory.java deleted file mode 100644 index 502c6dac998..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerNumberFormatFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2004-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.number; - -import java.text.NumberFormat; -import java.util.Locale; - -/** - * Produces NumberFormat instances that format integer values. - * @author Keith Donald - * @see NumberFormat - * @since 3.0 - */ -final class IntegerNumberFormatFactory extends NumberFormatFactory { - public NumberFormat getNumberFormat(Locale locale) { - return NumberFormat.getIntegerInstance(locale); - } -} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/NumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/NumberFormatFactory.java deleted file mode 100644 index 66f871afa06..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/NumberFormatFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2004-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.number; - -import java.text.NumberFormat; -import java.util.Locale; - -/** - * A factory for {@link NumberFormat} objects. - * Conceals the complexity associated with configuring, constructing, and/or caching number format instances. - * @author Keith Donald - * @since 3.0 - */ -abstract class NumberFormatFactory { - - /** - * Factory method that returns a fully-configured {@link NumberFormat} instance to use to format an object for - * display. - * @return the number format - */ - public abstract NumberFormat getNumberFormat(Locale locale); - -} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java index 71892853098..5d4da824a22 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java @@ -1,79 +1,45 @@ /* - * Copyright 2004-2009 the original author or authors. - * + * 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.number; -import java.math.BigDecimal; +import java.text.DecimalFormat; import java.text.NumberFormat; -import java.text.ParseException; -import java.text.ParsePosition; import java.util.Locale; -import org.springframework.ui.format.Formatter; - /** * A Number formatter for percent values. - * Delegates to {@link NumberFormat#getPercentInstance(Locale)}. + * + *

Delegates to {@link NumberFormat#getPercentInstance(Locale)}. * Configures BigDecimal parsing so there is no loss in precision. * The {@link #parse(String, Locale)} routine always returns a BigDecimal. + * * @author Keith Donald + * @author Juergen Hoeller * @since 3.0 - * @see #setLenient(boolean) + * @see #setLenient */ -public final class PercentFormatter implements Formatter { +public final class PercentFormatter extends AbstractNumberFormatter { - private PercentNumberFormatFactory percentFormatFactory = new PercentNumberFormatFactory(); - - private boolean lenient; - - /** - * Specify whether or not parsing is to be lenient. - * With lenient parsing, the parser may allow inputs that do not precisely match the format. - * With strict parsing, inputs must match the format exactly. - * Default is false. - */ - public void setLenient(boolean lenient) { - this.lenient = lenient; + protected NumberFormat getNumberFormat(Locale locale) { + NumberFormat format = NumberFormat.getPercentInstance(locale); + if (format instanceof DecimalFormat) { + ((DecimalFormat) format).setParseBigDecimal(true); + } + return format; } - public String format(Number number, Locale locale) { - if (number == null) { - return ""; - } - NumberFormat format = percentFormatFactory.getNumberFormat(locale); - return format.format(number); - } - - public Number parse(String formatted, Locale locale) - throws ParseException { - if (formatted.length() == 0) { - return null; - } - NumberFormat format = percentFormatFactory.getNumberFormat(locale); - ParsePosition position = new ParsePosition(0); - BigDecimal decimal = (BigDecimal) format.parse(formatted, position); - if (position.getErrorIndex() != -1) { - throw new ParseException(formatted, position.getIndex()); - } - if (!lenient) { - if (formatted.length() != position.getIndex()) { - // indicates a part of the string that was not parsed - throw new ParseException(formatted, position.getIndex()); - } - } - return decimal; - } - -} \ No newline at end of file +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentNumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentNumberFormatFactory.java deleted file mode 100644 index b6724a3e475..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentNumberFormatFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2004-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.number; - -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.util.Locale; - -/** - * Produces NumberFormat instances that format percent values. - * @see NumberFormat - * @author Keith Donald - * @since 3.0 - */ -final class PercentNumberFormatFactory extends NumberFormatFactory { - public NumberFormat getNumberFormat(Locale locale) { - DecimalFormat format = (DecimalFormat) NumberFormat.getPercentInstance(locale); - format.setParseBigDecimal(true); - return format; - } -} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/package-info.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/package-info.java index 421a7800b6c..9dc39b1147c 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/package-info.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/package-info.java @@ -2,4 +2,3 @@ * Formatters for java.lang.Number properties. */ package org.springframework.ui.format.number; - 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 837f699a09d..4cf55e896f6 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 @@ -16,16 +16,13 @@ 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.converter.Converter; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; import org.springframework.ui.format.Formatter; import org.springframework.ui.format.FormatterRegistry; -import org.springframework.ui.format.support.GenericFormatterRegistry; import org.springframework.util.Assert; /** @@ -36,12 +33,10 @@ import org.springframework.util.Assert; * @author Juergen Hoeller * @since 3.0 */ -public class FormattingConversionServiceAdapter implements ConversionService { +public class FormattingConversionServiceAdapter extends GenericConversionService { private final FormatterRegistry formatterRegistry; - private final ConversionService targetConversionService; - /** * Create a new FormattingConversionServiceAdapter for the given FormatterRegistry. @@ -51,41 +46,37 @@ public class FormattingConversionServiceAdapter implements ConversionService { Assert.notNull(formatterRegistry, "FormatterRegistry must not be null"); this.formatterRegistry = formatterRegistry; if (formatterRegistry instanceof GenericFormatterRegistry) { - this.targetConversionService = ((GenericFormatterRegistry) formatterRegistry).getConversionService(); + setParent(((GenericFormatterRegistry) formatterRegistry).getConversionService()); } else { - this.targetConversionService = new DefaultConversionService(); + setParent(new DefaultConversionService()); } } - public boolean canConvert(Class sourceType, Class targetType) { - return canConvert(sourceType, TypeDescriptor.valueOf(targetType)); - } - - public boolean canConvert(Class sourceType, TypeDescriptor targetType) { - return (this.formatterRegistry.getFormatter(targetType) != null || - this.targetConversionService.canConvert(sourceType, targetType)); - } - - @SuppressWarnings("unchecked") - public T convert(Object source, Class targetType) { - return (T) convert(source, TypeDescriptor.valueOf(targetType)); - } - - public Object convert(Object source, TypeDescriptor targetType) { - if (source instanceof String) { - Formatter formatter = this.formatterRegistry.getFormatter(targetType); + @Override + protected Converter findRegisteredConverter(Class sourceType, Class targetType) { + if (String.class.equals(sourceType)) { + Formatter formatter = this.formatterRegistry.getFormatter(targetType); if (formatter != null) { - try { - return formatter.parse((String) source, LocaleContextHolder.getLocale()); - } - catch (ParseException ex) { - throw new ConversionFailedException(source, String.class, targetType.getType(), ex); - } + return new FormattingConverter(formatter); } } - return this.targetConversionService.convert(source, targetType); + return super.findRegisteredConverter(sourceType, targetType); + } + + + private static class FormattingConverter implements Converter { + + private final Formatter formatter; + + public FormattingConverter(Formatter formatter) { + this.formatter = formatter; + } + + public T convert(String source) throws Exception { + return this.formatter.parse(source, 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 index 6015bd3f67c..7b47fbe9e63 100644 --- 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 @@ -27,22 +27,24 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; +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.util.Assert; -import org.springframework.ui.format.FormatterRegistry; -import org.springframework.ui.format.Formatter; 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. + * A generic implementation of {@link org.springframework.ui.format.FormatterRegistry} + * suitable for use in most environments. * * @author Keith Donald * @author Juergen Hoeller @@ -52,7 +54,7 @@ import org.springframework.ui.format.Formatted; * @see #add(Class, org.springframework.ui.format.Formatter) * @see #add(org.springframework.ui.format.AnnotationFormatterFactory) */ -public class GenericFormatterRegistry implements FormatterRegistry, BeanFactoryAware, Cloneable { +public class GenericFormatterRegistry implements FormatterRegistry, ApplicationContextAware, Cloneable { private final Map typeFormatters = new ConcurrentHashMap(); @@ -61,46 +63,59 @@ public class GenericFormatterRegistry implements FormatterRegistry, BeanFactoryA private ConversionService conversionService = new DefaultConversionService(); + private ApplicationContext applicationContext; + private boolean shared = true; /** * Registers the formatters in the set provided. - * JavaBean-friendly alternative to calling {@link #add(Formatter)}. + * JavaBean-friendly alternative to calling {@link #addFormatterByType(Formatter)}. * @see #add(Formatter) */ public void setFormatters(Set> formatters) { for (Formatter formatter : formatters) { - add(formatter); + addFormatterByType(formatter); } } /** * Registers the formatters in the map provided by type. - * JavaBean-friendly alternative to calling {@link #add(Class, Formatter)}. + * 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()) { - add(entry.getKey(), entry.getValue()); + 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 #add(AnnotationFormatterFactory)}. + * JavaBean-friendly alternative to calling {@link #addFormatterByAnnotation(AnnotationFormatterFactory)}. * @see #add(AnnotationFormatterFactory) */ - public void setAnnotationFormatterFactories(Set factories) { - for (AnnotationFormatterFactory factory : factories) { - add(factory); + 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 #add(Class, Formatter) + * @see #addFormatterByType(Class, Formatter) */ public void setConversionService(ConversionService conversionService) { Assert.notNull(conversionService, "ConversionService must not be null"); @@ -117,12 +132,13 @@ public class GenericFormatterRegistry implements FormatterRegistry, BeanFactoryA /** * Take the context's default ConversionService if none specified locally. */ - public void setBeanFactory(BeanFactory beanFactory) { + public void setApplicationContext(ApplicationContext context) { if (this.conversionService == null && - beanFactory.containsBean(ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME)) { - this.conversionService = beanFactory.getBean( + context.containsBean(ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME)) { + this.conversionService = context.getBean( ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME, ConversionService.class); } + this.applicationContext = context; } @@ -157,6 +173,7 @@ public class GenericFormatterRegistry implements FormatterRegistry, BeanFactoryA clone.typeFormatters.putAll(this.typeFormatters); clone.annotationFormatters.putAll(this.annotationFormatters); clone.conversionService = this.conversionService; + clone.applicationContext = applicationContext; clone.shared = false; return clone; } @@ -164,32 +181,48 @@ public class GenericFormatterRegistry implements FormatterRegistry, BeanFactoryA // implementing FormatterRegistry - public void add(Formatter formatter) { - this.typeFormatters.put(getFormattedObjectType(formatter.getClass()), formatter); - } - - public void add(Class type, Formatter formatter) { + public void addFormatterByType(Class type, Formatter formatter) { Class formattedObjectType = getFormattedObjectType(formatter.getClass()); if (!this.conversionService.canConvert(formattedObjectType, type)) { - throw new IllegalArgumentException("Unable to register formatter " + formatter + " for type [" + type.getName() + "]; not able to convert from [" + formattedObjectType.getName() + "] to parse"); + throw new IllegalArgumentException("Unable to register Formatter " + formatter + " for type [" + type.getName() + "]; not able to convert from [" + formattedObjectType.getName() + "] to parse"); } if (!this.conversionService.canConvert(type, formattedObjectType)) { - throw new IllegalArgumentException("Unable to register formatter " + formatter + " for type [" + type.getName() + "]; not able to convert to [" + formattedObjectType.getName() + "] to format"); + throw new IllegalArgumentException("Unable to register Formatter " + formatter + " for type [" + type.getName() + "]; not able to convert to [" + formattedObjectType.getName() + "] to format"); } this.typeFormatters.put(type, formatter); } - public void add(AnnotationFormatterFactory factory) { + public void addFormatterByType(Formatter formatter) { + Class formattedObjectType = getFormattedObjectType(formatter.getClass()); + this.typeFormatters.put(formattedObjectType, formatter); + } + + public void addFormatterByAnnotation(Class annotationType, Formatter formatter) { + this.annotationFormatters.put(annotationType, new SimpleAnnotationFormatterFactory(formatter)); + } + + public void addFormatterByAnnotation(AnnotationFormatterFactory factory) { this.annotationFormatters.put(getAnnotationType(factory.getClass()), factory); } + @SuppressWarnings("unchecked") + public Formatter getFormatter(Class targetType) { + return (Formatter) getFormatter(TypeDescriptor.valueOf(targetType)); + } + @SuppressWarnings("unchecked") public Formatter getFormatter(TypeDescriptor type) { - Assert.notNull(type, "The TypeDescriptor is required"); + Assert.notNull(type, "TypeDescriptor is required"); Formatter formatter = getAnnotationFormatter(type); if (formatter == null) { formatter = getTypeFormatter(type.getType()); } + if (formatter != null) { + Class formattedObjectType = getFormattedObjectType(formatter.getClass()); + if (!type.getType().isAssignableFrom(formattedObjectType)) { + return new ConvertingFormatter(type.getType(), formattedObjectType, formatter); + } + } return formatter; } @@ -250,37 +283,33 @@ public class GenericFormatterRegistry implements FormatterRegistry, BeanFactoryA classToIntrospect = classToIntrospect.getSuperclass(); } throw new IllegalArgumentException( - "Unable to extract Annotation type A argument from AnnotationFormatterFactory [" - + factoryClass.getName() + "]; does the factory parameterize the generic type?"); + "Unable to extract Annotation type A argument from AnnotationFormatterFactory [" + + factoryClass.getName() + "]; does the factory parameterize the generic type?"); } @SuppressWarnings("unchecked") private Formatter getAnnotationFormatter(TypeDescriptor type) { Annotation[] annotations = type.getAnnotations(); - for (Annotation a : annotations) { - AnnotationFormatterFactory factory = annotationFormatters.get(a.annotationType()); + for (Annotation ann : annotations) { + AnnotationFormatterFactory factory = this.annotationFormatters.get(ann.annotationType()); if (factory != null) { - return factory.getFormatter(a); + return factory.getFormatter(ann); + } + else { + Formatted formattedAnnotation = ann.annotationType().getAnnotation(Formatted.class); + if (formattedAnnotation != null) { + Formatter formatter = createFormatter(formattedAnnotation.value()); + this.annotationFormatters.put(ann.annotationType(), new SimpleAnnotationFormatterFactory(formatter)); + return formatter; + } } } return null; } - + private Formatter getTypeFormatter(Class type) { - Assert.notNull(type, "The Class of the object to format is required"); Formatter formatter = findFormatter(type); - if (formatter != null) { - Class formattedObjectType = getFormattedObjectType(formatter.getClass()); - if (type.isAssignableFrom(formattedObjectType)) { - return formatter; - } - else { - return new ConvertingFormatter(type, formattedObjectType, formatter); - } - } - else { - return getDefaultFormatter(type); - } + return (formatter != null ? formatter : getDefaultFormatter(type)); } private Formatter findFormatter(Class type) { @@ -288,7 +317,7 @@ public class GenericFormatterRegistry implements FormatterRegistry, BeanFactoryA classQueue.addFirst(type); while (!classQueue.isEmpty()) { Class currentClass = classQueue.removeLast(); - Formatter formatter = typeFormatters.get(currentClass); + Formatter formatter = this.typeFormatters.get(currentClass); if (formatter != null) { return formatter; } @@ -306,32 +335,29 @@ public class GenericFormatterRegistry implements FormatterRegistry, BeanFactoryA private Formatter getDefaultFormatter(Class type) { Formatted formatted = AnnotationUtils.findAnnotation(type, Formatted.class); if (formatted != null) { - Class formatterClass = formatted.value(); - try { - Formatter formatter = (Formatter) formatterClass.newInstance(); - typeFormatters.put(type, formatter); - return formatter; - } catch (InstantiationException e) { - throw new IllegalStateException( - "Formatter referenced by @Formatted annotation does not have default constructor", e); - } catch (IllegalAccessException e) { - throw new IllegalStateException( - "Formatter referenced by @Formatted annotation does not have public constructor", e); - } + Formatter formatter = createFormatter(formatted.value()); + this.typeFormatters.put(type, formatter); + return formatter; } else { return null; } } + private Formatter createFormatter(Class formatterClass) { + return (this.applicationContext != null ? + this.applicationContext.getAutowireCapableBeanFactory().createBean(formatterClass) : + BeanUtils.instantiate(formatterClass)); + } + private class ConvertingFormatter implements Formatter { - private Class type; + private final Class type; - private Class formattedObjectType; + private final Class formattedObjectType; - private Formatter targetFormatter; + private final Formatter targetFormatter; public ConvertingFormatter(Class type, Class formattedObjectType, Formatter targetFormatter) { this.type = type; @@ -341,16 +367,29 @@ public class GenericFormatterRegistry implements FormatterRegistry, BeanFactoryA @SuppressWarnings("unchecked") public String format(Object object, Locale locale) { - object = conversionService.convert(object, formattedObjectType); - return targetFormatter.format(object, locale); + object = conversionService.convert(object, this.formattedObjectType); + return this.targetFormatter.format(object, locale); } public Object parse(String formatted, Locale locale) throws ParseException { - Object parsed = targetFormatter.parse(formatted, locale); - parsed = conversionService.convert(parsed, type); + Object parsed = this.targetFormatter.parse(formatted, locale); + parsed = conversionService.convert(parsed, this.type); return parsed; } + } + + 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; + } } } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/package-info.java b/org.springframework.context/src/main/java/org/springframework/ui/package-info.java index dc7a3c854b6..77380dc8f3f 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/package-info.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/package-info.java @@ -1,9 +1,5 @@ - /** - * * Generic support for UI layer concepts. * Provides a generic ModelMap for model holding. - * */ package org.springframework.ui; - 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 0766361c627..d6eaee8a896 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 @@ -106,11 +106,7 @@ public abstract class AbstractPropertyBindingResult extends AbstractBindingResul @Override protected Object formatFieldValue(String field, Object value) { String fixedField = fixedField(field); - TypeDescriptor td = getPropertyAccessor().getPropertyTypeDescriptor(fixedField); - Formatter formatter = (this.formatterRegistry != null ? this.formatterRegistry.getFormatter(td) : null); - if (formatter != null) { - return formatter.format(value, LocaleContextHolder.getLocale()); - } + // Try custom editor... PropertyEditor customEditor = getCustomEditor(fixedField); if (customEditor != null) { customEditor.setValue(value); @@ -121,6 +117,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()); + } + // Nothing found: return value as-is. return value; } @@ -144,14 +147,18 @@ public abstract class AbstractPropertyBindingResult extends AbstractBindingResul */ @Override public PropertyEditor findEditor(String field, Class valueType) { - TypeDescriptor td = (valueType != null ? TypeDescriptor.valueOf(valueType) : - getPropertyAccessor().getPropertyTypeDescriptor(fixedField(field))); - final Formatter formatter = - (this.formatterRegistry != null ? this.formatterRegistry.getFormatter(td) : null); - if (formatter != null) { - return new FormattingPropertyEditorAdapter(formatter); + 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); + } } - return super.findEditor(field, 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 67b45026147..d4a22edc7f5 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 @@ -36,8 +36,8 @@ import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; import org.springframework.ui.format.FormatterRegistry; -import org.springframework.ui.format.support.GenericFormatterRegistry; 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; @@ -135,6 +135,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor(); + private Validator validator; + private FormatterRegistry formatterRegistry; @@ -443,6 +445,23 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { return this.bindingErrorProcessor; } + /** + * Set the Validator to apply after each binding step. + */ + public void setValidator(Validator validator) { + if (validator != null && (getTarget() != null && !validator.supports(getTarget().getClass()))) { + throw new IllegalStateException("Invalid target for Validator [" + validator + "]: " + getTarget()); + } + this.validator = validator; + } + + /** + * Return the Validator to apply after each binding step, if any. + */ + public Validator getValidator() { + return this.validator; + } + /** * Set the FormatterRegistry to use for obtaining Formatters in preference * to JavaBeans PropertyEditors. @@ -633,6 +652,18 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { } + /** + * Invoke the specified Validator, if any. + * @see #setValidator(Validator) + * @see #getBindingResult() + */ + public void validate() { + Validator validator = getValidator(); + if (validator != null) { + validator.validate(getTarget(), getBindingResult()); + } + } + /** * Close this DataBinder, which may result in throwing * a BindException if it encountered any errors. diff --git a/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java new file mode 100644 index 00000000000..be9d84edfa3 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java @@ -0,0 +1,74 @@ +/* + * 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.validation.beanvalidation; + +import java.util.Iterator; +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * @author Juergen Hoeller + * @since 3.0 + */ +public class BeanValidationPostProcessor implements BeanPostProcessor, InitializingBean { + + private javax.validation.Validator validator; + + + public void setValidator(Validator validator) { + this.validator = validator; + } + + public void setValidatorFactory(ValidatorFactory validatorFactory) { + this.validator = validatorFactory.getValidator(); + } + + public void afterPropertiesSet() { + if (this.validator == null) { + Validation.buildDefaultValidatorFactory().getValidator(); + } + } + + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + Set> result = this.validator.validate(bean); + if (!result.isEmpty()) { + StringBuilder sb = new StringBuilder("Bean state is invalid: "); + for (Iterator> it = result.iterator(); it.hasNext();) { + ConstraintViolation violation = it.next(); + sb.append(violation.getPropertyPath()).append(" - ").append(violation.getMessage()); + if (it.hasNext()) { + sb.append("; "); + } + } + throw new BeanInitializationException(sb.toString()); + } + return bean; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/CustomValidatorBean.java b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/CustomValidatorBean.java new file mode 100644 index 00000000000..63961d3d4e0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/CustomValidatorBean.java @@ -0,0 +1,83 @@ +/* + * 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.validation.beanvalidation; + +import javax.validation.MessageInterpolator; +import javax.validation.TraversableResolver; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorContext; +import javax.validation.ValidatorFactory; + +import org.springframework.beans.factory.InitializingBean; + +/** + * Configurable bean class that exposes a specific JSR-303 Validator + * through its original interface as well as through the Spring + * {@link org.springframework.validation.Validator} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class CustomValidatorBean extends SpringValidatorAdapter implements Validator, InitializingBean { + + private ValidatorFactory validatorFactory; + + private MessageInterpolator messageInterpolator; + + private TraversableResolver traversableResolver; + + + /** + * Set the ValidatorFactory to obtain the target Validator from. + *

Default is {@link javax.validation.Validation#buildDefaultValidatorFactory()}. + */ + public void setValidatorFactory(ValidatorFactory validatorFactory) { + this.validatorFactory = validatorFactory; + } + + /** + * Specify a custom MessageInterpolator to use for this Validator. + */ + public void setMessageInterpolator(MessageInterpolator messageInterpolator) { + this.messageInterpolator = messageInterpolator; + } + + /** + * Specify a custom TraversableResolver to use for this Validator. + */ + public void setTraversableResolver(TraversableResolver traversableResolver) { + this.traversableResolver = traversableResolver; + } + + + public void afterPropertiesSet() { + if (this.validatorFactory == null) { + this.validatorFactory = Validation.buildDefaultValidatorFactory(); + } + + ValidatorContext validatorContext = this.validatorFactory.usingContext(); + validatorContext.messageInterpolator(new LocaleContextMessageInterpolator( + this.messageInterpolator, this.validatorFactory.getMessageInterpolator())); + if (this.traversableResolver != null) { + validatorContext.traversableResolver(this.traversableResolver); + } + + setTargetValidator(validatorContext.getValidator()); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java new file mode 100644 index 00000000000..aee2db8667e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java @@ -0,0 +1,209 @@ +/* + * 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.validation.beanvalidation; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import javax.validation.Configuration; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.MessageInterpolator; +import javax.validation.TraversableResolver; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorContext; +import javax.validation.ValidatorFactory; +import javax.validation.spi.ValidationProvider; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.Resource; +import org.springframework.util.CollectionUtils; + +/** + * This is the central class for javax.validation (JSR-303) setup + * in a Spring application context: It bootstraps a javax.validation.ValidationFactory + * and exposes it through the Spring {@link org.springframework.validation.Validator} interface + * as well as through the JSR-303 {@link javax.validation.Validator} interface and the + * {@link javax.validation.ValidatorFactory} interface itself. + * + *

When talking to an instance of this bean through the Spring or JSR-303 Validator interfaces, + * you'll be talking to the default Validator of the underlying ValidatorFactory. This is very + * convenient in that you don't have to perform yet another call on the factory, assuming that + * you will almost always use the default Validator anyway. This can also be injected directly + * into any target dependency of type {@link org.springframework.validation.Validator}! + * + * @author Juergen Hoeller + * @since 3.0 + * @see javax.validation.ValidatorFactory + * @see javax.validation.Validator + * @see javax.validation.Validation#buildDefaultValidatorFactory() + */ +public class LocalValidatorFactoryBean extends SpringValidatorAdapter + implements ValidatorFactory, Validator, ApplicationContextAware, InitializingBean { + + private Class providerClass; + + private MessageInterpolator messageInterpolator; + + private TraversableResolver traversableResolver; + + private ConstraintValidatorFactory constraintValidatorFactory; + + private Resource[] mappingLocations; + + private final Map validationPropertyMap = new HashMap(); + + private ApplicationContext applicationContext; + + private ValidatorFactory validatorFactory; + + + /** + * Specify the desired provider class, if any. + *

If not specified, JSR-303's default search mechanism will be used. + * @see javax.validation.Validation#byProvider(Class) + * @see javax.validation.Validation#byDefaultProvider() + */ + public void setProviderClass(Class providerClass) { + this.providerClass = providerClass; + } + + /** + * Specify a custom MessageInterpolator to use for this ValidatorFactory + * and its exposed default Validator. + */ + public void setMessageInterpolator(MessageInterpolator messageInterpolator) { + this.messageInterpolator = messageInterpolator; + } + + /** + * Specify a custom TraversableResolver to use for this ValidatorFactory + * and its exposed default Validator. + */ + public void setTraversableResolver(TraversableResolver traversableResolver) { + this.traversableResolver = traversableResolver; + } + + /** + * Specify a custom ConstraintValidatorFactory to use for this ValidatorFactory. + *

Default is a {@link SpringConstraintValidatorFactory}, delegating to the + * containing ApplicationContext for creating autowired ConstraintValidator instances. + */ + public void setConstraintValidatorFactory(ConstraintValidatorFactory constraintValidatorFactory) { + this.constraintValidatorFactory = constraintValidatorFactory; + } + + /** + * Specify resource locations to load XML constraint mapping files from, if any. + */ + public void setMappingLocations(Resource[] mappingLocations) { + this.mappingLocations = mappingLocations; + } + + /** + * Specify bean validation properties to be passed to the validation provider. + *

Can be populated with a String + * "value" (parsed via PropertiesEditor) or a "props" element in XML bean definitions. + * @see javax.validation.Configuration#addProperty(String, String) + */ + public void setValidationProperties(Properties jpaProperties) { + CollectionUtils.mergePropertiesIntoMap(jpaProperties, this.validationPropertyMap); + } + + /** + * Specify bean validation properties to be passed to the validation provider as a Map. + *

Can be populated with a + * "map" or "props" element in XML bean definitions. + * @see javax.validation.Configuration#addProperty(String, String) + */ + public void setValidationPropertyMap(Map validationProperties) { + if (validationProperties != null) { + this.validationPropertyMap.putAll(validationProperties); + } + } + + /** + * Allow Map access to the bean validation properties to be passed to the validation provider, + * with the option to add or override specific entries. + *

Useful for specifying entries directly, for example via "validationPropertyMap[myKey]". + */ + public Map getValidationPropertyMap() { + return this.validationPropertyMap; + } + + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + + public void afterPropertiesSet() { + Configuration configuration = (this.providerClass != null ? + Validation.byProvider(this.providerClass).configure() : + Validation.byDefaultProvider().configure()); + + configuration.messageInterpolator(new LocaleContextMessageInterpolator( + this.messageInterpolator, configuration.getDefaultMessageInterpolator())); + + if (this.traversableResolver != null) { + configuration.traversableResolver(this.traversableResolver); + } + + ConstraintValidatorFactory targetConstraintValidatorFactory = this.constraintValidatorFactory; + if (targetConstraintValidatorFactory == null && this.applicationContext != null) { + targetConstraintValidatorFactory = + new SpringConstraintValidatorFactory(this.applicationContext.getAutowireCapableBeanFactory()); + } + if (targetConstraintValidatorFactory != null) { + configuration.constraintValidatorFactory(targetConstraintValidatorFactory); + } + + if (this.mappingLocations != null) { + for (Resource location : this.mappingLocations) { + try { + configuration.addMapping(location.getInputStream()); + } + catch (IOException ex) { + throw new IllegalStateException("Cannot read mapping resource: " + location); + } + } + } + + for (Map.Entry entry : this.validationPropertyMap.entrySet()) { + configuration.addProperty(entry.getKey(), entry.getValue()); + } + + this.validatorFactory = configuration.buildValidatorFactory(); + setTargetValidator(this.validatorFactory.getValidator()); + } + + + public Validator getValidator() { + return this.validatorFactory.getValidator(); + } + + public ValidatorContext usingContext() { + return this.validatorFactory.usingContext(); + } + + public MessageInterpolator getMessageInterpolator() { + return this.validatorFactory.getMessageInterpolator(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/LocaleContextMessageInterpolator.java b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/LocaleContextMessageInterpolator.java new file mode 100644 index 00000000000..657bd2cf50a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/LocaleContextMessageInterpolator.java @@ -0,0 +1,60 @@ +/* + * 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.validation.beanvalidation; + +import java.util.Locale; +import javax.validation.MessageInterpolator; + +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.util.Assert; + +/** + * Delegates to a target {@link MessageInterpolator} implementation but enforces Spring's + * managed Locale. Typically used to wrap the validation provider's default interpolator. + * + * @author Juergen Hoeller + * @since 3.0 + * @see org.springframework.context.i18n.LocaleContextHolder#getLocale() + */ +public class LocaleContextMessageInterpolator implements MessageInterpolator { + + private final MessageInterpolator targetInterpolator; + + + /** + * Create a new LocaleContextMessageInterpolator, wrapping the given target interpolator. + * @param targetInterpolator the target MessageInterpolator to wrap + */ + public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator) { + Assert.notNull(targetInterpolator, "Target MessageInterpolator must not be null"); + this.targetInterpolator = targetInterpolator; + } + + LocaleContextMessageInterpolator(MessageInterpolator customInterpolator, MessageInterpolator defaultInterpolator) { + this.targetInterpolator = (customInterpolator != null ? customInterpolator : defaultInterpolator); + } + + + public String interpolate(String message, Context context) { + return this.targetInterpolator.interpolate(message, context, LocaleContextHolder.getLocale()); + } + + public String interpolate(String message, Context context, Locale locale) { + return this.targetInterpolator.interpolate(message, context, locale); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/SpringConstraintValidatorFactory.java b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/SpringConstraintValidatorFactory.java new file mode 100644 index 00000000000..0fc183316f5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/SpringConstraintValidatorFactory.java @@ -0,0 +1,53 @@ +/* + * 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.validation.beanvalidation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorFactory; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.util.Assert; + +/** + * JSR-303 {@link ConstraintValidatorFactory} implementation that delegates to a + * Spring BeanFactory for creating autowired {@link ConstraintValidator} instances. + * + * @author Juergen Hoeller + * @since 3.0 + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean(Class) + * @see org.springframework.context.ApplicationContext#getAutowireCapableBeanFactory() + */ +public class SpringConstraintValidatorFactory implements ConstraintValidatorFactory { + + private final AutowireCapableBeanFactory beanFactory; + + + /** + * Create a new SpringConstraintValidatorFactory for the given BeanFactory. + * @param beanFactory the target BeanFactory + */ + public SpringConstraintValidatorFactory(AutowireCapableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + } + + + public > T getInstance(Class key) { + return this.beanFactory.createBean(key); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java new file mode 100644 index 00000000000..60649a37a8b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.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.validation.beanvalidation; + +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.metadata.BeanDescriptor; + +import org.springframework.util.Assert; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +/** + * Adapter that takes a JSR-303 javax.validator.Validator + * and exposes it as a Spring {@link org.springframework.validation.Validator} + * while also exposing the original JSR-303 Validator interface itself. + * + *

Can be used as a programmatic wrapper. Also serves as base class for + * {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean}. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class SpringValidatorAdapter implements Validator, javax.validation.Validator { + + private javax.validation.Validator targetValidator; + + + /** + * Create a new SpringValidatorAdapter for the given JSR-303 Validator. + * @param targetValidator the JSR-303 Validator to wrap + */ + public SpringValidatorAdapter(javax.validation.Validator targetValidator) { + Assert.notNull(targetValidator, "Target Validator must not be null"); + this.targetValidator = targetValidator; + } + + SpringValidatorAdapter() { + } + + void setTargetValidator(javax.validation.Validator targetValidator) { + this.targetValidator = targetValidator; + } + + + //--------------------------------------------------------------------- + // Implementation of Spring Validator interface + //--------------------------------------------------------------------- + + public boolean supports(Class clazz) { + return true; + } + + public void validate(Object target, Errors errors) { + Set> result = this.targetValidator.validate(target); + for (ConstraintViolation violation : result) { + errors.rejectValue(violation.getPropertyPath().toString(), + violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(), + violation.getConstraintDescriptor().getAttributes().values().toArray(), + violation.getMessage()); + } + } + + + //--------------------------------------------------------------------- + // Implementation of JSR-303 Validator interface + //--------------------------------------------------------------------- + + public Set> validate(T object, Class... groups) { + return this.targetValidator.validate(object, groups); + } + + public Set> validateProperty(T object, String propertyName, Class... groups) { + return this.targetValidator.validateProperty(object, propertyName, groups); + } + + public Set> validateValue( + Class beanType, String propertyName, Object value, Class... groups) { + + return this.targetValidator.validateValue(beanType, propertyName, groups); + } + + public BeanDescriptor getConstraintsForClass(Class clazz) { + return this.targetValidator.getConstraintsForClass(clazz); + } + + public T unwrap(Class type) { + return this.targetValidator.unwrap(type); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/package-info.java b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/package-info.java new file mode 100644 index 00000000000..eb7815bb985 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/package-info.java @@ -0,0 +1,10 @@ +/** + * Support classes for integrating a JSR-303 Bean Validation provider + * (such as Hibernate Validator 4.0) into a Spring ApplicationContext + * and in particular with Spring's data binding and validation APIs. + * + *

The central class is {@link LocalValidatorFactoryBean} which + * defines a shared ValidatorFactory/Validator setup for availability + * to other Spring components. + */ +package org.springframework.validation.beanvalidation; diff --git a/org.springframework.context/src/main/java/org/springframework/validation/package-info.java b/org.springframework.context/src/main/java/org/springframework/validation/package-info.java index fc6fb191264..ce6b2dd243a 100644 --- a/org.springframework.context/src/main/java/org/springframework/validation/package-info.java +++ b/org.springframework.context/src/main/java/org/springframework/validation/package-info.java @@ -1,9 +1,5 @@ - /** - * * Provides data binding and validation functionality, * for usage in business and/or UI layers. - * */ package org.springframework.validation; - diff --git a/org.springframework.context/src/main/java/org/springframework/validation/support/package-info.java b/org.springframework.context/src/main/java/org/springframework/validation/support/package-info.java index 24b7214f744..9482069695d 100644 --- a/org.springframework.context/src/main/java/org/springframework/validation/support/package-info.java +++ b/org.springframework.context/src/main/java/org/springframework/validation/support/package-info.java @@ -1,8 +1,4 @@ - /** - * * Support classes for handling validation results. - * */ package org.springframework.validation.support; - diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/GenericFormatterRegistryTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/GenericFormatterRegistryTests.java index f4e9726f628..30e8ea1c6c0 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/format/GenericFormatterRegistryTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/format/GenericFormatterRegistryTests.java @@ -1,9 +1,21 @@ +/* + * 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 static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -13,14 +25,20 @@ import java.math.BigInteger; import java.text.ParseException; import java.util.Locale; +import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.style.ToStringCreator; 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; @@ -32,8 +50,8 @@ public class GenericFormatterRegistryTests { @Test public void testAdd() throws ParseException { - registry.add(new IntegerFormatter()); - Formatter formatter = registry.getFormatter(typeDescriptor(Integer.class)); + registry.addFormatterByType(new IntegerFormatter()); + Formatter formatter = registry.getFormatter(Integer.class); String formatted = formatter.format(new Integer(3), Locale.US); assertEquals("3", formatted); Integer i = (Integer) formatter.parse("3", Locale.US); @@ -42,23 +60,38 @@ public class GenericFormatterRegistryTests { @Test public void testAddByObjectType() { - registry.add(BigInteger.class, new IntegerFormatter()); - Formatter formatter = registry.getFormatter(typeDescriptor(BigInteger.class)); + registry.addFormatterByType(BigInteger.class, new IntegerFormatter()); + Formatter formatter = registry.getFormatter(BigInteger.class); String formatted = formatter.format(new BigInteger("3"), Locale.US); assertEquals("3", formatted); } @Test - public void testAddAnnotationFormatterFactory() throws Exception { - registry.add(new CurrencyAnnotationFormatterFactory()); + 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(Address.class)); + Formatter formatter = registry.getFormatter(Address.class); Address address = new Address(); address.street = "12345 Bel Aire Estates"; address.city = "Palm Bay"; @@ -70,33 +103,44 @@ public class GenericFormatterRegistryTests { @Test public void testGetNoFormatterForType() { - assertNull(registry.getFormatter(typeDescriptor(Integer.class))); + assertNull(registry.getFormatter(Integer.class)); } @Test(expected=IllegalArgumentException.class) public void testGetFormatterCannotConvert() { - registry.add(Integer.class, new AddressFormatter()); + registry.addFormatterByType(Integer.class, new AddressFormatter()); } + @Currency public BigDecimal currencyField; - private static TypeDescriptor typeDescriptor(Class clazz) { - return TypeDescriptor.valueOf(clazz); + @SmartCurrency + public BigDecimal smartCurrencyField; + + @Target({ElementType.METHOD, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface Currency { } - public static class CurrencyAnnotationFormatterFactory implements - AnnotationFormatterFactory { + @Target({ElementType.METHOD, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @Formatted(CurrencyFormatter.class) + public @interface SmartCurrency { + } + + public static class CurrencyAnnotationFormatterFactory implements AnnotationFormatterFactory { - private CurrencyFormatter currencyFormatter = new CurrencyFormatter(); + private final CurrencyFormatter currencyFormatter = new CurrencyFormatter(); - public Formatter getFormatter(Currency annotation) { - return 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; @@ -148,7 +192,7 @@ public class GenericFormatterRegistryTests { .append("zip", zip).toString(); } } - + public static class AddressFormatter implements Formatter

{ public String format(Address address, Locale locale) { @@ -164,14 +208,6 @@ public class GenericFormatterRegistryTests { address.setZip(fields[3]); return address; } - - } - - @Target( { ElementType.METHOD, ElementType.FIELD }) - @Retention(RetentionPolicy.RUNTIME) - @Documented - public @interface Currency { - } -} \ No newline at end of file +} 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 21f6762feda..698753735ef 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 @@ -26,6 +26,11 @@ 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; @@ -44,6 +49,8 @@ import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.context.support.StaticMessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.ui.format.number.DecimalFormatter; +import org.springframework.ui.format.Formatted; +import org.springframework.ui.format.support.GenericFormatterRegistry; import org.springframework.util.StringUtils; /** @@ -297,7 +304,7 @@ public class DataBinderTests extends TestCase { public void testBindingWithFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); - binder.getFormatterRegistry().add(Float.class, new DecimalFormatter()); + binder.getFormatterRegistry().addFormatterByType(Float.class, new DecimalFormatter()); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.addPropertyValue("myFloat", "1,2"); @@ -322,6 +329,45 @@ 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); + binder.setFormatterRegistry(new GenericFormatterRegistry()); + 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); @@ -1376,4 +1422,56 @@ 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.context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java b/org.springframework.context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java new file mode 100644 index 00000000000..e1bf2d0099d --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java @@ -0,0 +1,199 @@ +/* + * 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.validation.beanvalidation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Set; +import javax.validation.Constraint; +import javax.validation.ConstraintPayload; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintViolation; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +import org.hibernate.validation.HibernateValidationProvider; +import static org.junit.Assert.*; +import org.junit.Test; + +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +/** + * @author Juergen Hoeller + * @since 3.0 + */ +public class ValidatorFactoryTests { + + @Test + public void testSimpleValidation() throws Exception { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + ValidPerson person = new ValidPerson(); + Set> result = validator.validate(person); + assertEquals(2, result.size()); + for (ConstraintViolation cv : result) { + String path = cv.getPropertyPath().toString(); + if ("name".equals(path) || "address.street".equals(path)) { + assertTrue(cv.getConstraintDescriptor().getAnnotation() instanceof NotNull); + } + else { + fail("Invalid constraint violation with path '" + path + "'"); + } + } + } + + @Test + public void testSimpleValidationWithCustomProvider() throws Exception { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setProviderClass(HibernateValidationProvider.class); + validator.afterPropertiesSet(); + ValidPerson person = new ValidPerson(); + Set> result = validator.validate(person); + assertEquals(2, result.size()); + for (ConstraintViolation cv : result) { + String path = cv.getPropertyPath().toString(); + if ("name".equals(path) || "address.street".equals(path)) { + assertTrue(cv.getConstraintDescriptor().getAnnotation() instanceof NotNull); + } + else { + fail("Invalid constraint violation with path '" + path + "'"); + } + } + } + + @Test + public void testSimpleValidationWithClassLevel() throws Exception { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + ValidPerson person = new ValidPerson(); + person.setName("Juergen"); + person.getAddress().setStreet("Juergen's Street"); + Set> result = validator.validate(person); + assertEquals(1, result.size()); + Iterator> iterator = result.iterator(); + ConstraintViolation cv = iterator.next(); + assertEquals("", cv.getPropertyPath().toString()); + assertTrue(cv.getConstraintDescriptor().getAnnotation() instanceof NameAddressValid); + } + + @Test + public void testSpringValidation() throws Exception { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + ValidPerson person = new ValidPerson(); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertEquals(2, result.getErrorCount()); + FieldError fieldError = result.getFieldError("name"); + assertEquals("name", fieldError.getField()); + System.out.println(Arrays.asList(fieldError.getCodes())); + System.out.println(fieldError.getDefaultMessage()); + fieldError = result.getFieldError("address.street"); + assertEquals("address.street", fieldError.getField()); + System.out.println(Arrays.asList(fieldError.getCodes())); + System.out.println(fieldError.getDefaultMessage()); + } + + @Test + public void testSpringValidationWithClassLevel() throws Exception { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + ValidPerson person = new ValidPerson(); + person.setName("Juergen"); + person.getAddress().setStreet("Juergen's Street"); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertEquals(1, result.getErrorCount()); + ObjectError fieldError = result.getGlobalError(); + System.out.println(Arrays.asList(fieldError.getCodes())); + System.out.println(fieldError.getDefaultMessage()); + } + + + @NameAddressValid + private static class ValidPerson { + + @NotNull + private String name; + + @Valid + private ValidAddress address = new ValidAddress(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ValidAddress getAddress() { + return address; + } + + public void setAddress(ValidAddress address) { + this.address = address; + } + } + + + private static class ValidAddress { + + @NotNull + private String street; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Constraint(validatedBy = NameAddressValidator.class) + public @interface NameAddressValid { + + String message() default "Street must not contain name"; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + + public static class NameAddressValidator implements ConstraintValidator { + + public void initialize(NameAddressValid constraintAnnotation) { + } + + public boolean isValid(ValidPerson value, ConstraintValidatorContext constraintValidatorContext) { + return (value.name == null || !value.address.street.contains(value.name)); + } + } + +} diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/org.springframework.core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index b2f70947f70..6e61abd999e 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -37,7 +37,10 @@ import org.springframework.util.ClassUtils; */ public class TypeDescriptor { - public final static TypeDescriptor NULL = new TypeDescriptor((Class) null); + /** + * Constant defining an 'empty' TypeDescriptor. + */ + public static final TypeDescriptor NULL = new TypeDescriptor(); private Class type; @@ -50,17 +53,20 @@ public class TypeDescriptor { /** - * Creates a new descriptor for the given type. - * Use this constructor when a conversion point comes from a source such as a Map or collection, where no additional context is available. - * @param type the actual type + * Create a new descriptor for the given type. + *

Use this constructor when a conversion point comes from a source such as a Map + * or Collection, where no additional context is available. + * @param type the actual type to wrap */ public TypeDescriptor(Class type) { + Assert.notNull(type, "Type must not be null"); this.type = type; } /** * Create a new type descriptor from a method or constructor parameter. - * Use this constructor when a target conversion point originates from a method parameter, such as a setter method argument. + *

Use this constructor when a target conversion point originates from a method parameter, + * such as a setter method argument. * @param methodParameter the MethodParameter to wrap */ public TypeDescriptor(MethodParameter methodParameter) { @@ -78,6 +84,12 @@ public class TypeDescriptor { this.field = field; } + /** + * Internal constructor for a NULL descriptor. + */ + private TypeDescriptor() { + } + /** * Return the wrapped MethodParameter, if any. @@ -212,7 +224,7 @@ public class TypeDescriptor { public Annotation[] getAnnotations() { if (this.field != null) { if (this.cachedFieldAnnotations == null) { - this.cachedFieldAnnotations = field.getAnnotations(); + this.cachedFieldAnnotations = this.field.getAnnotations(); } return this.cachedFieldAnnotations; } @@ -317,7 +329,7 @@ public class TypeDescriptor { */ public static TypeDescriptor valueOf(Class type) { // TODO needs a cache for common type descriptors - return new TypeDescriptor(type); + return (type != null ? new TypeDescriptor(type) : NULL); } /** diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java b/org.springframework.core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java index 675a2a80df8..eecf9800dce 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.core.convert.converter; /** @@ -25,7 +26,7 @@ package org.springframework.core.convert.converter; * for example {@link Number} for a set of number subtypes. */ public interface ConverterFactory { - + /** * Get the converter to convert from S to target type T, where T is also an instance of R. * @param the target type diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java b/org.springframework.core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java index eb1ff6552b0..6e370f56231 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java @@ -24,14 +24,14 @@ package org.springframework.core.convert.converter; public interface ConverterRegistry { /** - * Add a converter to this registry. + * Add a plain converter to this registry. */ - void add(Converter converter); + void addConverter(Converter converter); /** - * Add a converter factory to this registry. + * Add a factory-created converter to this registry. */ - void add(ConverterFactory converterFactory); + void addConverter(ConverterFactory converterFactory); /** * Remove the conversion logic from the sourceType to the targetType. diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java index 6f492c974eb..33e622d0573 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java @@ -38,7 +38,7 @@ import org.springframework.util.NumberUtils; * @see java.math.BigDecimal * @see NumberUtils */ -public class CharacterToNumberFactory implements ConverterFactory { +class CharacterToNumberFactory implements ConverterFactory { public Converter getConverter(Class targetType) { return new CharacterToNumber(targetType); 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 c2649ac31e9..5eb60fc9eb6 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 @@ -21,6 +21,7 @@ package org.springframework.core.convert.support; * converters for a number of standard Java types like Class, Number, Boolean and so on. * * @author Keith Donald + * @author Juergen Hoeller * @since 3.0 */ public class DefaultConversionService extends GenericConversionService { @@ -29,22 +30,22 @@ public class DefaultConversionService extends GenericConversionService { * Create a new default conversion service, installing the default converters. */ public DefaultConversionService() { - add(new StringToByte()); - add(new StringToBoolean()); - add(new StringToCharacter()); - add(new StringToShort()); - add(new StringToInteger()); - add(new StringToLong()); - add(new StringToFloat()); - add(new StringToDouble()); - add(new StringToBigInteger()); - add(new StringToBigDecimal()); - add(new StringToLocale()); - add(new NumberToCharacter()); - add(new ObjectToString()); - add(new StringToEnumFactory()); - add(new NumberToNumberFactory()); - add(new CharacterToNumberFactory()); + addConverter(new StringToByte()); + addConverter(new StringToBoolean()); + addConverter(new StringToCharacter()); + addConverter(new StringToShort()); + addConverter(new StringToInteger()); + addConverter(new StringToLong()); + addConverter(new StringToFloat()); + addConverter(new StringToDouble()); + addConverter(new StringToBigInteger()); + addConverter(new StringToBigDecimal()); + addConverter(new StringToLocale()); + addConverter(new NumberToCharacter()); + addConverter(new ObjectToString()); + addConverter(new StringToEnumFactory()); + addConverter(new NumberToNumberFactory()); + addConverter(new CharacterToNumberFactory()); } } 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 26d3bae6de7..b195186ffd7 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 @@ -45,8 +45,8 @@ import org.springframework.util.ClassUtils; * @author Keith Donald * @author Juergen Hoeller * @since 3.0 - * @see #add(Converter) - * @see #add(ConverterFactory) + * @see #addConverter(Converter) + * @see #addConverter(ConverterFactory) */ public class GenericConversionService implements ConversionService, ConverterRegistry { @@ -61,23 +61,23 @@ public class GenericConversionService implements ConversionService, ConverterReg /** * Registers the converters in the set provided. - * JavaBean-friendly alternative to calling {@link #add(Converter)}. - * @see #add(Converter) + * JavaBean-friendly alternative to calling {@link #addConverter(Converter)}. + * @see #addConverter(Converter) */ public void setConverters(Set converters) { for (Converter converter : converters) { - add(converter); + addConverter(converter); } } /** * Registers the converters in the set provided. - * JavaBean-friendly alternative to calling {@link #add(ConverterFactory)}. - * @see #add(ConverterFactory) + * JavaBean-friendly alternative to calling {@link #addConverter(ConverterFactory)}. + * @see #addConverter(ConverterFactory) */ public void setConverterFactories(Set converters) { for (ConverterFactory converterFactory : converters) { - add(converterFactory); + addConverter(converterFactory); } } @@ -98,7 +98,7 @@ public class GenericConversionService implements ConversionService, ConverterReg // implementing ConverterRegistry - public void add(Converter converter) { + public void addConverter(Converter converter) { List typeInfo = getRequiredTypeInfo(converter); if (typeInfo == null) { throw new IllegalArgumentException("Unable to the determine sourceType and targetType your Converter converts between"); @@ -108,7 +108,7 @@ public class GenericConversionService implements ConversionService, ConverterReg getSourceMap(sourceType).put(targetType, converter); } - public void add(ConverterFactory converterFactory) { + public void addConverter(ConverterFactory converterFactory) { List typeInfo = getRequiredTypeInfo(converterFactory); if (typeInfo == null) { throw new IllegalArgumentException("Unable to the determine sourceType and targetType your ConverterFactory creates Converters to convert between"); @@ -153,12 +153,12 @@ public class GenericConversionService implements ConversionService, ConverterReg } else { throw new ConverterNotFoundException(source.getClass(), targetType.getType(), - "No converter found that can convert from sourceType [" + source.getClass().getName() - + "] to targetType [" + targetType.getName() + "]"); + "No converter found that can convert from source type [" + source.getClass().getName() + + "] to target type [" + targetType.getName() + "]"); } } - ConversionExecutor getConversionExecutor(Class sourceClass, TypeDescriptor targetType) + ConversionExecutor getConversionExecutor(final Class sourceClass, final TypeDescriptor targetType) throws ConverterNotFoundException { Assert.notNull(sourceClass, "The sourceType to convert from is required"); @@ -175,8 +175,8 @@ public class GenericConversionService implements ConversionService, ConverterReg else if (targetType.isCollection()) { if (targetType.isAbstractClass()) { throw new IllegalArgumentException("Conversion target class [" + targetType.getName() - + "] is invalid; cannot convert to abstract collection types--" - + "request an interface or concrete implementation instead"); + + "] is invalid; cannot convert to abstract collection types. " + + "Request an interface or a concrete implementation instead!"); } return new ArrayToCollection(sourceType, targetType, this); } @@ -190,9 +190,8 @@ public class GenericConversionService implements ConversionService, ConverterReg } } else { - if (targetType.getType().equals(String.class)) { - // array to string - return null; + if (sourceType.getElementType().equals(String.class)) { + return new StringArrayToObject(targetType, this); } else { // array to object @@ -210,14 +209,15 @@ public class GenericConversionService implements ConversionService, ConverterReg else if (targetType.isMap()) { if (sourceType.getElementType().equals(String.class)) { return new StringCollectionToMap(sourceType, targetType, this); - } else { + } + else { // object collection to map return null; } } else { if (targetType.getType().equals(String.class)) { - // collection to string; + // collection to string return null; } else { @@ -255,7 +255,7 @@ public class GenericConversionService implements ConversionService, ConverterReg } if (targetType.isArray()) { if (sourceType.getType().equals(String.class)) { - return new StringToArray(sourceType, targetType, this); + return new StringToArray(targetType, this); } else { return new ObjectToArray(sourceType, targetType, this); @@ -281,10 +281,22 @@ public class GenericConversionService implements ConversionService, ConverterReg if (sourceType.isAssignableTo(targetType)) { return NoOpConversionExecutor.INSTANCE; } - Converter converter = findRegisteredConverter(ClassUtils.resolvePrimitiveIfNecessary(sourceType.getType()), ClassUtils.resolvePrimitiveIfNecessary(targetType.getType())); + Converter converter = findRegisteredConverter( + ClassUtils.resolvePrimitiveIfNecessary(sourceClass), + ClassUtils.resolvePrimitiveIfNecessary(targetType.getType())); if (converter != null) { return new StaticConversionExecutor(sourceType, targetType, converter); } + else if (this.parent instanceof GenericConversionService) { + return ((GenericConversionService) this.parent).getConversionExecutor(sourceClass, targetType); + } + else if (this.parent != null && this.parent.canConvert(sourceClass, targetType)){ + return new ConversionExecutor() { + public Object execute(Object source) { + return parent.convert(source, targetType); + } + }; + } else { return null; } @@ -356,7 +368,7 @@ public class GenericConversionService implements ConversionService, ConverterReg return sourceMap; } - private Converter findRegisteredConverter(Class sourceType, Class targetType) { + protected Converter findRegisteredConverter(Class sourceType, Class targetType) { if (sourceType.isInterface()) { LinkedList classQueue = new LinkedList(); classQueue.addFirst(sourceType); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/NumberToCharacter.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/NumberToCharacter.java index 1e8693f075d..1db34868359 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/NumberToCharacter.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/NumberToCharacter.java @@ -34,7 +34,7 @@ import org.springframework.util.NumberUtils; * @see java.math.BigDecimal * @see NumberUtils */ -public class NumberToCharacter implements Converter { +class NumberToCharacter implements Converter { public Character convert(Number source) { return (char) source.shortValue(); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/NumberToNumberFactory.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/NumberToNumberFactory.java index bce1848c85d..0f3c1969b1e 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/NumberToNumberFactory.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/NumberToNumberFactory.java @@ -38,7 +38,7 @@ import org.springframework.util.NumberUtils; * @see java.math.BigDecimal * @see NumberUtils */ -public class NumberToNumberFactory implements ConverterFactory { +class NumberToNumberFactory implements ConverterFactory { public Converter getConverter(Class targetType) { return new NumberToNumber(targetType); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/ObjectToArray.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ObjectToArray.java index 3bc1baec150..92ace3eae5c 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/ObjectToArray.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ObjectToArray.java @@ -30,7 +30,7 @@ import org.springframework.core.convert.TypeDescriptor; */ class ObjectToArray implements ConversionExecutor { - private final TypeDescriptor targetArrayType; + private final Class elementType; private final ConversionExecutor elementConverter; @@ -38,15 +38,15 @@ class ObjectToArray implements ConversionExecutor { public ObjectToArray(TypeDescriptor sourceObjectType, TypeDescriptor targetArrayType, GenericConversionService conversionService) { - this.targetArrayType = targetArrayType; + this.elementType = targetArrayType.getElementType(); this.elementConverter = conversionService.getConversionExecutor( - sourceObjectType.getType(), TypeDescriptor.valueOf(targetArrayType.getElementType())); + sourceObjectType.getType(), TypeDescriptor.valueOf(this.elementType)); } public Object execute(Object source) throws ConversionFailedException { - Object array = Array.newInstance(targetArrayType.getElementType(), 1); - Object element = elementConverter.execute(source); + Object array = Array.newInstance(this.elementType, 1); + Object element = this.elementConverter.execute(source); Array.set(array, 0, element); return array; } diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/ObjectToString.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ObjectToString.java index 85c0de3f67e..385d704d54c 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/ObjectToString.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ObjectToString.java @@ -26,7 +26,7 @@ import org.springframework.core.convert.converter.Converter; * @author Keith Donald * @since 3.0 */ -public class ObjectToString implements Converter { +class ObjectToString implements Converter { public String convert(Object source) { return source.toString(); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringArrayToObject.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringArrayToObject.java new file mode 100644 index 00000000000..ac477fb379c --- /dev/null +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringArrayToObject.java @@ -0,0 +1,45 @@ +/* + * 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 org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Converts a String array to a single element. + * + * @author Juergen Hoeller + * @since 3.0 + */ +class StringArrayToObject implements ConversionExecutor { + + private final ConversionExecutor elementConverter; + + + public StringArrayToObject(TypeDescriptor targetType, GenericConversionService conversionService) { + this.elementConverter = conversionService.getConversionExecutor(String.class, targetType); + } + + + public Object execute(Object source) throws ConversionFailedException { + String str = StringUtils.arrayToCommaDelimitedString(ObjectUtils.toObjectArray(source)); + return this.elementConverter.execute(str); + } + +} diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToArray.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToArray.java index f55b9180e80..a52c2fba57b 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToArray.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToArray.java @@ -17,6 +17,7 @@ package org.springframework.core.convert.support; import org.springframework.core.convert.TypeDescriptor; +import org.springframework.util.StringUtils; /** * Converts a comma-delimited string to an array. @@ -27,15 +28,18 @@ import org.springframework.core.convert.TypeDescriptor; @SuppressWarnings("unchecked") class StringToArray implements ConversionExecutor { - private ArrayToArray converter; + private final ArrayToArray converter; - public StringToArray(TypeDescriptor sourceType, TypeDescriptor targetType, GenericConversionService conversionService) { - converter = new ArrayToArray(TypeDescriptor.valueOf(String[].class), targetType, conversionService); + + public StringToArray(TypeDescriptor targetType, GenericConversionService conversionService) { + this.converter = new ArrayToArray(TypeDescriptor.valueOf(String[].class), targetType, conversionService); } + public Object execute(Object source) { - String string = (String) source; - String[] fields = string.split(","); - return converter.execute(fields); + String str = (String) source; + String[] fields = StringUtils.commaDelimitedListToStringArray(str); + return this.converter.execute(fields); } + } diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBigDecimal.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBigDecimal.java index 16b8c88e781..c4688402274 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBigDecimal.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBigDecimal.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.core.convert.support; import java.math.BigDecimal; @@ -26,10 +27,10 @@ import org.springframework.util.NumberUtils; * @author Keith Donald * @since 3.0 */ -public class StringToBigDecimal implements Converter { +class StringToBigDecimal implements Converter { public BigDecimal convert(String source) { return NumberUtils.parseNumber(source, BigDecimal.class); } -} \ No newline at end of file +} diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBigInteger.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBigInteger.java index c3e58478b61..edec532913d 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBigInteger.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBigInteger.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.core.convert.support; import java.math.BigInteger; @@ -26,10 +27,10 @@ import org.springframework.util.NumberUtils; * @author Keith Donald * @since 3.0 */ -public class StringToBigInteger implements Converter { +class StringToBigInteger implements Converter { public BigInteger convert(String source) { return NumberUtils.parseNumber(source, BigInteger.class); } -} \ No newline at end of file +} diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBoolean.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBoolean.java index 678f5b2c808..6e0d0960812 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBoolean.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToBoolean.java @@ -13,17 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.core.convert.support; import org.springframework.core.convert.converter.Converter; /** - * Converts String to a Boolean.. + * Converts String to a Boolean. * * @author Keith Donald * @since 3.0 */ -public class StringToBoolean implements Converter { +class StringToBoolean implements Converter { public Boolean convert(String source) { if (source.equals("true")) { diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToByte.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToByte.java index 7d6217d1271..dd8b156b6d4 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToByte.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToByte.java @@ -25,10 +25,10 @@ import org.springframework.util.NumberUtils; * @author Keith Donald * @since 3.0 */ -public class StringToByte implements Converter { +class StringToByte implements Converter { public Byte convert(String source) { return NumberUtils.parseNumber(source, Byte.class); } -} \ No newline at end of file +} diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToCharacter.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToCharacter.java index 0490a102fd9..0167f6498bd 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToCharacter.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToCharacter.java @@ -24,7 +24,7 @@ import org.springframework.core.convert.converter.Converter; * @author Keith Donald * @since 3.0 */ -public class StringToCharacter implements Converter { +class StringToCharacter implements Converter { public Character convert(String source) { if (source.length() != 1) { diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToCollection.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToCollection.java index ee948ca8377..1179c6cbe67 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToCollection.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToCollection.java @@ -21,22 +21,25 @@ import org.springframework.core.convert.TypeDescriptor; /** * Converts a comma-delimited string to a collection. + * * @author Keith Donald * @since 3.0 */ @SuppressWarnings("unchecked") class StringToCollection implements ConversionExecutor { - private ArrayToCollection converter; + private final ArrayToCollection converter; + public StringToCollection(TypeDescriptor sourceType, TypeDescriptor targetType, GenericConversionService conversionService) { - converter = new ArrayToCollection(sourceType, targetType, conversionService); + this.converter = new ArrayToCollection(sourceType, targetType, conversionService); } + public Object execute(Object source) throws ConversionFailedException { String string = (String) source; String[] fields = string.split(","); - return converter.execute(fields); + return this.converter.execute(fields); } } diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToDouble.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToDouble.java index f453f3dbd22..7b431c76dcb 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToDouble.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToDouble.java @@ -25,7 +25,7 @@ import org.springframework.util.NumberUtils; * @author Keith Donald * @since 3.0 */ -public class StringToDouble implements Converter { +class StringToDouble implements Converter { public Double convert(String source) { return NumberUtils.parseNumber(source, Double.class); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToEnumFactory.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToEnumFactory.java index c683a93bdc2..4f48bf8673f 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToEnumFactory.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToEnumFactory.java @@ -27,7 +27,7 @@ import org.springframework.core.convert.converter.ConverterInfo; * @since 3.0 */ @SuppressWarnings("unchecked") -public class StringToEnumFactory implements ConverterFactory { +class StringToEnumFactory implements ConverterFactory { public Converter getConverter(Class targetType) { return new StringToEnum(targetType); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToFloat.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToFloat.java index 00b23effd18..31cc441937f 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToFloat.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToFloat.java @@ -25,7 +25,7 @@ import org.springframework.util.NumberUtils; * @author Keith Donald * @since 3.0 */ -public class StringToFloat implements Converter { +class StringToFloat implements Converter { public Float convert(String source) { return NumberUtils.parseNumber(source, Float.class); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToInteger.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToInteger.java index 7b93c13a300..ef8fe1d3e33 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToInteger.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToInteger.java @@ -25,7 +25,7 @@ import org.springframework.util.NumberUtils; * @author Keith Donald * @since 3.0 */ -public class StringToInteger implements Converter { +class StringToInteger implements Converter { public Integer convert(String source) { return NumberUtils.parseNumber(source, Integer.class); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToLocale.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToLocale.java index 28ec1ba8cd4..1f18f4d639c 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToLocale.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToLocale.java @@ -27,7 +27,7 @@ import org.springframework.util.StringUtils; * @author Keith Donald * @since 3.0 */ -public class StringToLocale implements Converter { +class StringToLocale implements Converter { public Locale convert(String source) { return StringUtils.parseLocaleString(source); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToLong.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToLong.java index df03ddc998a..7e5a9090e9c 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToLong.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToLong.java @@ -25,7 +25,7 @@ import org.springframework.util.NumberUtils; * @author Keith Donald * @since 3.0 */ -public class StringToLong implements Converter { +class StringToLong implements Converter { public Long convert(String source) { return NumberUtils.parseNumber(source, Long.class); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToShort.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToShort.java index 166d6d5c858..b214251ac84 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToShort.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToShort.java @@ -25,7 +25,7 @@ import org.springframework.util.NumberUtils; * @author Keith Donald * @since 3.0 */ -public class StringToShort implements Converter { +class StringToShort implements Converter { public Short convert(String source) { return NumberUtils.parseNumber(source, Short.class); diff --git a/org.springframework.core/src/main/java/org/springframework/util/ClassUtils.java b/org.springframework.core/src/main/java/org/springframework/util/ClassUtils.java index 390489de125..be907f20c08 100644 --- a/org.springframework.core/src/main/java/org/springframework/util/ClassUtils.java +++ b/org.springframework.core/src/main/java/org/springframework/util/ClassUtils.java @@ -776,7 +776,7 @@ public abstract class ClassUtils { * @param clazz the class to check * @return the original class, or a primitive wrapper for the original primitive type */ - public static Class resolvePrimitiveIfNecessary(Class clazz) { + public static Class resolvePrimitiveIfNecessary(Class clazz) { Assert.notNull(clazz, "Class must not be null"); return (clazz.isPrimitive() ? primitiveTypeToWrapperMap.get(clazz) : clazz); } diff --git a/org.springframework.core/src/main/java/org/springframework/util/StringUtils.java b/org.springframework.core/src/main/java/org/springframework/util/StringUtils.java index db5b860347b..6864c83e468 100644 --- a/org.springframework.core/src/main/java/org/springframework/util/StringUtils.java +++ b/org.springframework.core/src/main/java/org/springframework/util/StringUtils.java @@ -356,7 +356,7 @@ public abstract class StringUtils { } int count = 0; int pos = 0; - int idx = 0; + int idx; while ((idx = str.indexOf(sub, pos)) != -1) { ++count; pos = idx + sub.length(); @@ -997,7 +997,7 @@ public abstract class StringUtils { } else { int pos = 0; - int delPos = 0; + int delPos; while ((delPos = str.indexOf(delimiter, pos)) != -1) { result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); pos = delPos + delimiter.length(); @@ -1090,6 +1090,9 @@ public abstract class StringUtils { if (ObjectUtils.isEmpty(arr)) { return ""; } + if (arr.length == 1) { + return ObjectUtils.nullSafeToString(arr[0]); + } StringBuilder sb = new StringBuilder(); for (int i = 0; i < arr.length; i++) { if (i > 0) { diff --git a/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java b/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java index 22f49c79415..34b47910856 100644 --- a/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java +++ b/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java @@ -42,7 +42,7 @@ public class GenericConversionServiceTests { @Test public void executeConversion() { - converter.add(new StringToInteger()); + converter.addConverter(new StringToInteger()); assertEquals(new Integer(3), converter.convert("3", Integer.class)); } @@ -68,7 +68,7 @@ public class GenericConversionServiceTests { @Test public void addConverterNoSourceTargetClassInfoAvailable() { try { - converter.add(new Converter() { + converter.addConverter(new Converter() { public Object convert(Object source) throws Exception { return source; } @@ -97,7 +97,7 @@ public class GenericConversionServiceTests { @Test public void convertWrongTypeArgument() { - converter.add(new StringToInteger()); + converter.addConverter(new StringToInteger()); try { converter.convert("BOGUS", Integer.class); fail("Should have failed"); @@ -108,7 +108,7 @@ public class GenericConversionServiceTests { @Test public void convertSuperSourceType() { - converter.add(new Converter() { + converter.addConverter(new Converter() { public Integer convert(CharSequence source) throws Exception { return Integer.valueOf(source.toString()); } @@ -119,14 +119,14 @@ public class GenericConversionServiceTests { @Test public void convertObjectToPrimitive() { - converter.add(new StringToInteger()); + converter.addConverter(new StringToInteger()); Integer three = converter.convert("3", int.class); assertEquals(3, three.intValue()); } @Test public void convertArrayToArray() { - converter.add(new StringToInteger()); + converter.addConverter(new StringToInteger()); Integer[] result = converter.convert(new String[] { "1", "2", "3" }, Integer[].class); assertEquals(new Integer(1), result[0]); assertEquals(new Integer(2), result[1]); @@ -135,7 +135,7 @@ public class GenericConversionServiceTests { @Test public void convertArrayToPrimitiveArray() { - converter.add(new StringToInteger()); + converter.addConverter(new StringToInteger()); int[] result = converter.convert(new String[] { "1", "2", "3" }, int[].class); assertEquals(1, result[0]); assertEquals(2, result[1]); @@ -154,7 +154,7 @@ public class GenericConversionServiceTests { @Test public void convertArrayToListGenericTypeConversion() throws Exception { - converter.add(new StringToInteger()); + converter.addConverter(new StringToInteger()); List result = (List) converter.convert(new String[] { "1", "2", "3" }, new TypeDescriptor(getClass().getDeclaredField("genericList"))); assertEquals(new Integer("1"), result.get(0)); assertEquals(new Integer("2"), result.get(1)); @@ -192,7 +192,7 @@ public class GenericConversionServiceTests { @Test public void convertListToArrayWithComponentConversion() { - converter.add(new StringToInteger()); + converter.addConverter(new StringToInteger()); List list = new ArrayList(); list.add("1"); list.add("2"); @@ -210,8 +210,8 @@ public class GenericConversionServiceTests { Map foo = new HashMap(); foo.put("1", "BAR"); foo.put("2", "BAZ"); - converter.add(new StringToInteger()); - converter.add(new StringToEnumFactory().getConverter(FooEnum.class)); + converter.addConverter(new StringToInteger()); + converter.addConverter(new StringToEnumFactory().getConverter(FooEnum.class)); Map map = (Map) converter.convert(foo, new TypeDescriptor(getClass().getField("genericMap"))); assertEquals(map.get(1), FooEnum.BAR); assertEquals(map.get(2), FooEnum.BAZ); @@ -228,7 +228,7 @@ public class GenericConversionServiceTests { @Test public void convertStringToArrayWithElementConversion() { - converter.add(new StringToInteger()); + converter.addConverter(new StringToInteger()); Integer[] result = converter.convert("1,2,3", Integer[].class); assertEquals(3, result.length); assertEquals(new Integer(1), result[0]); diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index 4304cc88d93..532df2f1499 100644 --- a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -54,7 +54,6 @@ import javax.servlet.http.Cookie; import org.springframework.beans.BeanUtils; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; -import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.style.StylerUtils; @@ -66,7 +65,6 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.support.BindingAwareModelMap; -import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; @@ -83,7 +81,6 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.portlet.HandlerAdapter; import org.springframework.web.portlet.ModelAndView; import org.springframework.web.portlet.bind.MissingPortletRequestParameterException; -import org.springframework.web.portlet.bind.PortletRequestDataBinder; import org.springframework.web.portlet.bind.annotation.ActionMapping; import org.springframework.web.portlet.bind.annotation.EventMapping; import org.springframework.web.portlet.bind.annotation.RenderMapping; @@ -335,26 +332,6 @@ public class AnnotationMethodHandlerAdapter extends PortletContentGenerator impl return mav; } - - /** - * Template method for creating a new PortletRequestDataBinder instance. - *

The default implementation creates a standard PortletRequestDataBinder. - * This can be overridden for custom PortletRequestDataBinder subclasses. - * @param request current portlet request - * @param target the target object to bind onto (or null - * if the binder is just used to convert a plain parameter value) - * @param objectName the objectName of the target object - * @return the PortletRequestDataBinder instance to use - * @throws Exception in case of invalid state or arguments - * @see PortletRequestDataBinder#bind(javax.portlet.PortletRequest) - * @see PortletRequestDataBinder#convertIfNecessary(Object, Class, MethodParameter) - */ - protected PortletRequestDataBinder createBinder( - PortletRequest request, Object target, String objectName) throws Exception { - - return new PortletRequestDataBinder(target, objectName); - } - /** * Build a HandlerMethodResolver for the given handler type. */ @@ -579,25 +556,6 @@ public class AnnotationMethodHandlerAdapter extends PortletContentGenerator impl throw new PortletSessionRequiredException(message); } - @Override - protected WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) - throws Exception { - - return AnnotationMethodHandlerAdapter.this.createBinder( - (PortletRequest) webRequest.getNativeRequest(), target, objectName); - } - - @Override - protected void doBind(NativeWebRequest webRequest, WebDataBinder binder, boolean failOnErrors) - throws Exception { - - PortletRequestDataBinder servletBinder = (PortletRequestDataBinder) binder; - servletBinder.bind((PortletRequest) webRequest.getNativeRequest()); - if (failOnErrors) { - servletBinder.closeNoCatch(); - } - } - @Override protected Object resolveCookieValue(String cookieName, Class paramType, NativeWebRequest webRequest) throws Exception { diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index ed076edf888..156ff84bc2f 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -47,7 +47,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanUtils; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; -import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpInputMessage; @@ -74,8 +73,6 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.HttpSessionRequiredException; import org.springframework.web.bind.MissingServletRequestParameterException; -import org.springframework.web.bind.ServletRequestDataBinder; -import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; @@ -373,25 +370,6 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen return -1; } - /** - * Template method for creating a new ServletRequestDataBinder instance. - *

The default implementation creates a standard ServletRequestDataBinder. - * This can be overridden for custom ServletRequestDataBinder subclasses. - * @param request current HTTP request - * @param target the target object to bind onto (or null - * if the binder is just used to convert a plain parameter value) - * @param objectName the objectName of the target object - * @return the ServletRequestDataBinder instance to use - * @throws Exception in case of invalid state or arguments - * @see ServletRequestDataBinder#bind(javax.servlet.ServletRequest) - * @see ServletRequestDataBinder#convertIfNecessary(Object, Class, MethodParameter) - */ - protected ServletRequestDataBinder createBinder(HttpServletRequest request, Object target, String objectName) - throws Exception { - - return new ServletRequestDataBinder(target, objectName); - } - /** * Build a HandlerMethodResolver for the given handler type. */ @@ -617,25 +595,6 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen throw new HttpSessionRequiredException(message); } - @Override - protected WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) - throws Exception { - - return AnnotationMethodHandlerAdapter.this - .createBinder((HttpServletRequest) webRequest.getNativeRequest(), target, objectName); - } - - @Override - protected void doBind(NativeWebRequest webRequest, WebDataBinder binder, boolean failOnErrors) - throws Exception { - - ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; - servletBinder.bind((ServletRequest) webRequest.getNativeRequest()); - if (failOnErrors) { - servletBinder.closeNoCatch(); - } - } - @Override protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception { HttpServletRequest servletRequest = (HttpServletRequest) webRequest.getNativeRequest(); 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 a402872276b..30e3e0b9583 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 @@ -42,6 +42,8 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; import static org.junit.Assert.*; import org.junit.Test; @@ -73,12 +75,13 @@ 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.support.GenericFormatterRegistry; 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.validation.BindingResult; import org.springframework.validation.Errors; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.InitBinder; @@ -433,13 +436,13 @@ public class ServletAnnotationControllerTests { @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); - wac.registerBeanDefinition("controller", - new RootBeanDefinition(MyCommandProvidingFormController.class)); + 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); @@ -469,8 +472,7 @@ public class ServletAnnotationControllerTests { wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)); RootBeanDefinition adapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); adapterDef.getPropertyValues().addPropertyValue("webBindingInitializer", new MyWebBindingInitializer()); - adapterDef.getPropertyValues() - .addPropertyValue("customArgumentResolver", new MySpecialArgumentResolver()); + adapterDef.getPropertyValues().addPropertyValue("customArgumentResolver", new MySpecialArgumentResolver()); wac.registerBeanDefinition("handlerAdapter", adapterDef); wac.refresh(); return wac; @@ -1227,6 +1229,20 @@ public class ServletAnnotationControllerTests { } } + public static class ValidTestBean extends TestBean { + + @NotNull + private String validCountry; + + public void setValidCountry(String validCountry) { + this.validCountry = validCountry; + } + + public String getValidCountry() { + return this.validCountry; + } + } + @Controller public static class MyModelFormController { @@ -1253,16 +1269,20 @@ public class ServletAnnotationControllerTests { @SuppressWarnings("unused") @ModelAttribute("myCommand") - private TestBean createTestBean(@RequestParam T defaultName, - Map model, - @RequestParam Date date) { + private ValidTestBean createTestBean(@RequestParam T defaultName, + Map model, @RequestParam Date date) { model.put("myKey", "myOriginalValue"); - return new TestBean(defaultName.getClass().getSimpleName() + ":" + defaultName.toString()); + ValidTestBean tb = new ValidTestBean(); + tb.setName(defaultName.getClass().getSimpleName() + ":" + defaultName.toString()); + return tb; } @Override @RequestMapping("/myPath.do") - public String myHandle(@ModelAttribute("myCommand") TestBean tb, BindingResult errors, ModelMap model) { + public String myHandle(@ModelAttribute("myCommand") @Valid TestBean tb, BindingResult errors, ModelMap model) { + if (!errors.hasFieldErrors("validCountry")) { + throw new IllegalStateException("Declarative validation not applied"); + } return super.myHandle(tb, errors, model); } @@ -1306,6 +1326,9 @@ public class ServletAnnotationControllerTests { @InitBinder private void initBinder(WebDataBinder binder) { binder.initBeanPropertyAccess(); + LocalValidatorFactoryBean vf = new LocalValidatorFactoryBean(); + vf.afterPropertiesSet(); + binder.setValidator(vf); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); @@ -1319,6 +1342,9 @@ public class ServletAnnotationControllerTests { @SuppressWarnings("unused") @InitBinder({"myCommand", "date"}) private void initBinder(WebDataBinder binder, String date, @RequestParam("date") String[] date2) { + LocalValidatorFactoryBean vf = new LocalValidatorFactoryBean(); + vf.afterPropertiesSet(); + binder.setValidator(vf); assertEquals("2007-10-02", date); assertEquals(1, date2.length); assertEquals("2007-10-02", date2[0]); @@ -1331,6 +1357,9 @@ public class ServletAnnotationControllerTests { private static class MyWebBindingInitializer implements WebBindingInitializer { public void initBinder(WebDataBinder binder, WebRequest request) { + LocalValidatorFactoryBean vf = new LocalValidatorFactoryBean(); + vf.afterPropertiesSet(); + binder.setValidator(vf); assertNotNull(request.getLocale()); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); @@ -1496,8 +1525,8 @@ public class ServletAnnotationControllerTests { if (tb == null) { tb = (TestBean) model.get("myCommand"); } - if (tb.getName().endsWith("myDefaultName")) { - assertTrue(tb.getDate().getYear() == 107); + if (tb.getName() != null && tb.getName().endsWith("myDefaultName")) { + assertEquals(107, tb.getDate().getYear()); } Errors errors = (Errors) model.get(BindingResult.MODEL_KEY_PREFIX + "testBean"); if (errors == null) { @@ -1701,11 +1730,8 @@ public class ServletAnnotationControllerTests { public static class MyModelAndViewResolver implements ModelAndViewResolver { - public ModelAndView resolveModelAndView(Method handlerMethod, - Class handlerType, - Object returnValue, - ExtendedModelMap implicitModel, - NativeWebRequest webRequest) { + public ModelAndView resolveModelAndView(Method handlerMethod, Class handlerType, + Object returnValue, ExtendedModelMap implicitModel, NativeWebRequest webRequest) { if (returnValue instanceof MySpecialArg) { return new ModelAndView(new View() { public String getContentType() { @@ -1736,7 +1762,6 @@ public class ServletAnnotationControllerTests { public void param(@RequestParam("myParam") int myParam, Writer writer) throws IOException { writer.write("myParam-" + myParam); } - } @Controller @@ -1757,7 +1782,6 @@ public class ServletAnnotationControllerTests { assertEquals("Invalid path variable value", new Date(108, 10, 18), date); writer.write("test-" + date.getYear()); } - } } diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java index 6137a4adc0c..d015d341437 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java @@ -29,6 +29,7 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.BeanUtils; import org.springframework.core.BridgeMethodResolver; import org.springframework.core.Conventions; @@ -172,6 +173,7 @@ public class HandlerMethodInvoker { String attrName = null; boolean required = false; String defaultValue = null; + boolean validate = false; int found = 0; Annotation[] paramAnns = methodParam.getParameterAnnotations(); @@ -211,6 +213,9 @@ public class HandlerMethodInvoker { attrName = attr.value(); found++; } + else if ("Valid".equals(paramAnn.annotationType().getSimpleName())) { + validate = true; + } } if (found > 1) { @@ -260,10 +265,10 @@ public class HandlerMethodInvoker { args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler); } else if (attrName != null) { - WebDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); + WebRequestDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); if (binder.getTarget() != null) { - doBind(webRequest, binder, !assignBindingResult); + doBind(binder, webRequest, validate, !assignBindingResult); } args[i] = binder.getTarget(); if (assignBindingResult) { @@ -401,7 +406,7 @@ public class HandlerMethodInvoker { } paramValue = checkValue(paramName, paramValue, paramType); } - WebDataBinder binder = createBinder(webRequest, null, paramName); + WebRequestDataBinder binder = new WebRequestDataBinder(null, paramName); initBinder(handlerForInitBinderCall, paramName, binder, webRequest); return binder.convertIfNecessary(paramValue, paramType, methodParam); } @@ -428,7 +433,7 @@ public class HandlerMethodInvoker { } headerValue = checkValue(headerName, headerValue, paramType); } - WebDataBinder binder = createBinder(webRequest, null, headerName); + WebRequestDataBinder binder = new WebRequestDataBinder(null, headerName); initBinder(handlerForInitBinderCall, headerName, binder, webRequest); return binder.convertIfNecessary(headerValue, paramType, methodParam); } @@ -495,7 +500,7 @@ public class HandlerMethodInvoker { } cookieValue = checkValue(cookieName, cookieValue, paramType); } - WebDataBinder binder = createBinder(webRequest, null, cookieName); + WebRequestDataBinder binder = new WebRequestDataBinder(null, cookieName); initBinder(handlerForInitBinderCall, cookieName, binder, webRequest); return binder.convertIfNecessary(cookieValue, paramType, methodParam); } @@ -518,7 +523,7 @@ public class HandlerMethodInvoker { pathVarName = getRequiredParameterName(methodParam); } String pathVarValue = resolvePathVariable(pathVarName, paramType, webRequest); - WebDataBinder binder = createBinder(webRequest, null, pathVarName); + WebRequestDataBinder binder = new WebRequestDataBinder(null, pathVarName); initBinder(handlerForInitBinderCall, pathVarName, binder, webRequest); return binder.convertIfNecessary(pathVarValue, paramType, methodParam); } @@ -557,7 +562,7 @@ public class HandlerMethodInvoker { return value; } - private WebDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam, + private WebRequestDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam, ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) throws Exception { @@ -580,7 +585,7 @@ public class HandlerMethodInvoker { else { bindObject = BeanUtils.instantiateClass(paramType); } - WebDataBinder binder = createBinder(webRequest, bindObject, name); + WebRequestDataBinder binder = new WebRequestDataBinder(bindObject, name); initBinder(handler, name, binder, webRequest); return binder; } @@ -609,7 +614,7 @@ public class HandlerMethodInvoker { (isSessionAttr || isBindingCandidate(attrValue))) { String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + attrName; if (mavModel != null && !model.containsKey(bindingResultKey)) { - WebDataBinder binder = createBinder(webRequest, attrValue, attrName); + WebRequestDataBinder binder = new WebRequestDataBinder(attrValue, attrName); initBinder(handler, attrName, binder, webRequest); mavModel.put(bindingResultKey, binder.getBindingResult()); } @@ -653,15 +658,15 @@ public class HandlerMethodInvoker { throw new IllegalStateException(message); } - protected WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception { - return new WebRequestDataBinder(target, objectName); - } + protected void doBind(WebRequestDataBinder binder, NativeWebRequest webRequest, boolean validate, boolean failOnErrors) + throws Exception { - protected void doBind(NativeWebRequest webRequest, WebDataBinder binder, boolean failOnErrors) throws Exception { - WebRequestDataBinder requestBinder = (WebRequestDataBinder) binder; - requestBinder.bind(webRequest); + binder.bind(webRequest); + if (validate) { + binder.validate(); + } if (failOnErrors) { - requestBinder.closeNoCatch(); + binder.closeNoCatch(); } } 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 cb4c09b3ace..d0cb13e3df2 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 @@ -20,6 +20,7 @@ import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.ui.format.FormatterRegistry; import org.springframework.validation.BindingErrorProcessor; import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.Validator; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.WebRequest; @@ -43,6 +44,8 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer private BindingErrorProcessor bindingErrorProcessor; + private Validator validator; + private FormatterRegistry formatterRegistry; private PropertyEditorRegistrar[] propertyEditorRegistrars; @@ -93,6 +96,20 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer return this.bindingErrorProcessor; } + /** + * Set the Validator to apply after each binding step. + */ + public final void setValidator(Validator validator) { + this.validator = validator; + } + + /** + * Return the Validator to apply after each binding step, if any. + */ + public final Validator getValidator() { + return this.validator; + } + /** * Specify a FormatterRegistry which will apply to every DataBinder. */ @@ -139,6 +156,10 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer if (this.bindingErrorProcessor != null) { binder.setBindingErrorProcessor(this.bindingErrorProcessor); } + if (this.validator != null && binder.getTarget() != null && + this.validator.supports(binder.getTarget().getClass())) { + binder.setValidator(this.validator); + } if (this.formatterRegistry != null) { binder.setFormatterRegistry(this.formatterRegistry); }