SPR-7912 Add tests for FormattingConversionServiceFactoryBean, update reference docs, and remove mvc:formatters

This commit is contained in:
Rossen Stoyanchev 2011-01-27 11:26:19 +00:00
parent 149348c907
commit abff2b959b
7 changed files with 350 additions and 192 deletions

View File

@ -0,0 +1,201 @@
/*
* Copyright 2002-2011 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.format.support;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.text.ParseException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.junit.Test;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.AnnotationFormatterFactory;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistrar;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.annotation.NumberFormat;
import org.springframework.format.annotation.NumberFormat.Style;
/**
* Test fixture for FormattingConversionServiceFactoryBean.
* @author Rossen Stoyanchev
*/
public class FormattingConversionServiceFactoryBeanTests {
@Test
public void testDefaultFormattersOn() throws Exception {
FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean();
factory.afterPropertiesSet();
FormattingConversionService fcs = factory.getObject();
TypeDescriptor descriptor = new TypeDescriptor(TestBean.class.getDeclaredField("percent"));
Object value = fcs.convert("5%", TypeDescriptor.valueOf(String.class), descriptor);
assertEquals(.05, value);
value = fcs.convert(.05, descriptor, TypeDescriptor.valueOf(String.class));
assertEquals("5%", value);
}
@Test
public void testDefaultFormattersOff() throws Exception {
FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean();
factory.setRegisterDefaultFormatters(false);
factory.afterPropertiesSet();
FormattingConversionService fcs = factory.getObject();
TypeDescriptor descriptor = new TypeDescriptor(TestBean.class.getDeclaredField("percent"));
try {
fcs.convert("5%", TypeDescriptor.valueOf(String.class), descriptor);
fail("This format should not be parseable");
} catch (ConversionFailedException e) {
assertTrue(e.getCause() instanceof NumberFormatException);
}
}
@Test
public void testCustomFormatter() throws Exception {
FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean();
Set<Object> formatters = new HashSet<Object>();
formatters.add(new TestBeanFormatter());
formatters.add(new SpecialIntAnnotationFormatterFactory());
factory.setFormatters(formatters);
factory.afterPropertiesSet();
FormattingConversionService fcs = factory.getObject();
TestBean testBean = fcs.convert("5", TestBean.class);
assertEquals(5, testBean.getSpecialInt());
assertEquals("5", fcs.convert(testBean, String.class));
TypeDescriptor descriptor = new TypeDescriptor(TestBean.class.getDeclaredField("specialInt"));
Object value = fcs.convert(":5", TypeDescriptor.valueOf(String.class), descriptor);
assertEquals(5, value);
value = fcs.convert(5, descriptor, TypeDescriptor.valueOf(String.class));
assertEquals(":5", value);
}
@Test
public void testFormatterRegistrar() throws Exception {
FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean();
Set<FormatterRegistrar> registrars = new HashSet<FormatterRegistrar>();
registrars.add(new TestFormatterRegistrar());
factory.setFormatterRegistrars(registrars);
factory.afterPropertiesSet();
FormattingConversionService fcs = factory.getObject();
TestBean testBean = fcs.convert("5", TestBean.class);
assertEquals(5, testBean.getSpecialInt());
assertEquals("5", fcs.convert(testBean, String.class));
}
@Test
public void testInvalidFormatter() throws Exception {
FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean();
Set<Object> formatters = new HashSet<Object>();
formatters.add(new Object());
factory.setFormatters(formatters);
try {
factory.afterPropertiesSet();
fail("Expected formatter to be rejected");
} catch (IllegalArgumentException e) {
// expected
}
}
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
private @interface SpecialInt {
}
private static class TestBean {
@SuppressWarnings("unused")
@NumberFormat(style = Style.PERCENT)
private double percent;
@SpecialInt
private int specialInt;
public int getSpecialInt() {
return specialInt;
}
public void setSpecialInt(int field) {
this.specialInt = field;
}
}
private static class TestBeanFormatter implements Formatter<TestBean> {
public String print(TestBean object, Locale locale) {
return String.valueOf(object.getSpecialInt());
}
public TestBean parse(String text, Locale locale) throws ParseException {
TestBean object = new TestBean();
object.setSpecialInt(Integer.parseInt(text));
return object;
}
}
private static class SpecialIntAnnotationFormatterFactory implements AnnotationFormatterFactory<SpecialInt> {
private final Set<Class<?>> fieldTypes = new HashSet<Class<?>>(1);
public SpecialIntAnnotationFormatterFactory() {
fieldTypes.add(Integer.class);
}
public Set<Class<?>> getFieldTypes() {
return fieldTypes;
}
public Printer<?> getPrinter(SpecialInt annotation, Class<?> fieldType) {
return new Printer<Integer>() {
public String print(Integer object, Locale locale) {
return ":" + object.toString();
}
};
}
public Parser<?> getParser(SpecialInt annotation, Class<?> fieldType) {
return new Parser<Integer>() {
public Integer parse(String text, Locale locale) throws ParseException {
return Integer.parseInt(text.substring(1));
}
};
}
}
private static class TestFormatterRegistrar implements FormatterRegistrar {
public void registerFormatters(FormatterRegistry registry) {
registry.addFormatter(new TestBeanFormatter());
}
}
}

View File

@ -38,25 +38,6 @@
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="formatters">
<xsd:annotation>
<xsd:documentation><![CDATA[
Registers custom Formatter and AnnotationFormatterFactory types with the FormattingConversionService.
Specifying custom formatters does not cancel the ones already built-in.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element ref="beans:bean" minOccurs="1" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
The Formatter or the AnnotationFormatterFactory bean definition.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:all>
<xsd:attribute name="conversion-service" type="xsd:string">
<xsd:annotation>

View File

@ -19,34 +19,16 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.text.ParseException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.format.AnnotationFormatterFactory;
import org.springframework.format.Formatter;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver;
@ -85,21 +67,6 @@ public class AnnotationDrivenBeanDefinitionParserTests {
verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerExceptionResolver.class));
}
@Test
public void testFormatters() throws Exception {
FormattingConversionService conversionService = appContext.getBean(FormattingConversionService.class);
assertNotNull(conversionService);
TestBean testBean = conversionService.convert("5", TestBean.class);
assertEquals(TestBeanFormatter.class.getSimpleName() + " should have been used.", 5, testBean.getField());
assertEquals("5", conversionService.convert(testBean, String.class));
TypeDescriptor intTypeDescriptor = new TypeDescriptor(TestBean.class.getDeclaredField("anotherField"));
Object actual = conversionService.convert(">>5<<", TypeDescriptor.valueOf(String.class), intTypeDescriptor);
assertEquals(TestBeanAnnotationFormatterFactory.class.getSimpleName() + " should have been used", 5, actual);
actual = conversionService.convert(5, intTypeDescriptor, TypeDescriptor.valueOf(String.class));
assertEquals(">>5<<", actual);
}
private void verifyMessageConverters(Object bean) {
assertNotNull(bean);
@ -124,102 +91,4 @@ public class AnnotationDrivenBeanDefinitionParserTests {
}
@SuppressWarnings("unused")
private static class TestWebArgumentResolver implements WebArgumentResolver {
public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception {
throw new IllegalStateException("Not expected to be invoked");
}
}
@SuppressWarnings("unused")
private static class AnotherTestWebArgumentResolver implements WebArgumentResolver {
public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception {
throw new IllegalStateException("Not expected to be invoked");
}
}
private static class TestBeanFormatter implements Formatter<TestBean> {
public String print(TestBean object, Locale locale) {
return String.valueOf(object.getField());
}
public TestBean parse(String text, Locale locale) throws ParseException {
TestBean object = new TestBean();
object.setField(Integer.parseInt(text));
return object;
}
}
private static class TestBeanAnnotationFormatterFactory implements AnnotationFormatterFactory<SpecialIntFormat> {
private final Set<Class<?>> fieldTypes = new HashSet<Class<?>>(1);
@SuppressWarnings("unused")
public TestBeanAnnotationFormatterFactory() {
fieldTypes.add(Integer.class);
}
public Set<Class<?>> getFieldTypes() {
return fieldTypes;
}
public Printer<?> getPrinter(SpecialIntFormat annotation, Class<?> fieldType) {
return new Printer<Integer>() {
public String print(Integer object, Locale locale) {
return ">>" + object.toString() + "<<";
}
};
}
public Parser<?> getParser(SpecialIntFormat annotation, Class<?> fieldType) {
return new Parser<Integer>() {
public Integer parse(String text, Locale locale) throws ParseException {
if (!text.startsWith(">>") || !text.endsWith("<<") || (text.length() < 5)) {
throw new ParseException(text + " is not in the expected format '>>intValue<<'", 0);
}
return Integer.parseInt(text.substring(2,3));
}
};
}
}
private static class TestBean {
private int field;
@SpecialIntFormat
private int anotherField;
public int getField() {
return field;
}
public void setField(int field) {
this.field = field;
}
@SuppressWarnings("unused")
public int getAnotherField() {
return anotherField;
}
@SuppressWarnings("unused")
public void setAnotherField(int anotherField) {
this.anotherField = anotherField;
}
}
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
private @interface SpecialIntFormat {
}
}

View File

@ -10,10 +10,6 @@
<bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
<bean class="org.springframework.http.converter.ResourceHttpMessageConverter"/>
</mvc:message-converters>
<mvc:formatters>
<bean class="org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParserTests$TestBeanFormatter"/>
<bean class="org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParserTests$TestBeanAnnotationFormatterFactory"/>
</mvc:formatters>
</mvc:annotation-driven>
<bean id="messageCodesResolver"

View File

@ -1279,7 +1279,7 @@ public void handle(@RequestBody String body, Writer writer) throws IOException {
the <classname>AnnotationMethodHandlerAdapter</classname> is extended
to support the <classname>@RequestBody</classname> and has the
following <interfacename>HttpMessageConverters</interfacename>
registered by default:</para>
registered by default if not using the MVC namespace:</para>
<itemizedlist>
<listitem>
@ -1301,16 +1301,12 @@ public void handle(@RequestBody String body, Writer writer) throws IOException {
<para><classname>SourceHttpMessageConverter</classname> converts
to/from a javax.xml.transform.Source.</para>
</listitem>
<listitem>
<para><classname>MarshallingHttpMessageConverter</classname>
converts to/from an object using the
<classname>org.springframework.oxm</classname> package.</para>
</listitem>
</itemizedlist>
<para>For more information on these converters, see <link
linkend="rest-message-conversion">Message Converters</link>.</para>
linkend="rest-message-conversion">Message Converters</link>.
Also see <xref linkend="mvc-annotation-driven"/> for information
on the default message converters set up by the MVC namespace.</para>
<para>The <classname>MarshallingHttpMessageConverter</classname>
requires a <interfacename>Marshaller</interfacename> and
@ -3313,13 +3309,69 @@ public class SimpleController {
</listitem>
<listitem>
<para>
Support for reading and writing XML, if JAXB is present on the classpath.
HttpMessageConverter support for @RequestBody method parameters and @ResponseBody method return values.
</para>
</listitem>
<listitem>
<para>
Support for reading and writing JSON, if Jackson is present on the classpath.
This is the complete list of HttpMessageConverters set up by mvc:annotation-driven:
<itemizedlist>
<listitem>
<para><classname>ByteArrayHttpMessageConverter</classname>
converts byte arrays.</para>
</listitem>
<listitem>
<para><classname>StringHttpMessageConverter</classname> converts
strings.</para>
</listitem>
<listitem>
<para><classname>ResourceHttpMessageConverter</classname> converts
to/from <classname>org.springframework.core.io.Resource</classname>
for all media types.</para>
</listitem>
<listitem>
<para><classname>SourceHttpMessageConverter</classname> converts
to/from a <classname>javax.xml.transform.Source</classname>.</para>
</listitem>
<listitem>
<para><classname>FormHttpMessageConverter</classname> converts
form data to/from a <classname>MultiValueMap&lt;String, String&gt;</classname>.</para>
</listitem>
<listitem>
<para><classname>Jaxb2RootElementHttpMessageConverter</classname>
converts Java objects to/from XML -- added if JAXB2 is present
on the classpath.
</para>
</listitem>
<listitem>
<para><classname>MappingJacksonHttpMessageConverter</classname>
converts to/from JSON -- added if Jackson is present on the classpath.
</para>
</listitem>
<listitem>
<para><classname>AtomFeedHttpMessageConverter</classname>
converts Atom feeds -- added if Rome is present on the classpath.
</para>
</listitem>
<listitem>
<para><classname>RssChannelHttpMessageConverter</classname>
converts RSS feeds -- added if Rome is present on the classpath.
</para>
</listitem>
</itemizedlist>
</para>
<note>
<para>
This list of HttpMessageConverters used can be replaced through
the mvc:message-converters sub-element of mvc:annotation-driven.
</para>
</note>
</listitem>
</orderedlist>
A typical usage is shown below:

View File

@ -172,17 +172,23 @@
<surname>Gierke</surname>
</author>
<author>
<firstname>Rossen</firstname>
<surname>Stoyanchev</surname>
</author>
</authorgroup>
<copyright>
<year>2004-2010</year>
<year>2004-2011</year>
<holder>Rod Johnson, Juergen Hoeller, Keith Donald, Colin Sampaleanu,
Rob Harrop, Alef Arendsen, Thomas Risberg, Darren Davison, Dmitriy
Kopylenko, Mark Pollack, Thierry Templier, Erwin Vervaet, Portia Tung,
Ben Hale, Adrian Colyer, John Lewis, Costin Leau, Mark Fisher, Sam
Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo,
Andy Clement, Dave Syer, Oliver Gierke</holder>
Andy Clement, Dave Syer, Oliver Gierke, Rossen Stoyanchev</holder>
</copyright>
<legalnotice>

View File

@ -1350,41 +1350,72 @@ public interface AnnotationFormatterFactory<A extends Annotation> {
<section id="format-FormatterRegistry-SPI">
<title>FormatterRegistry SPI</title>
<para> At runtime, Formatters are registered in a FormatterRegistry. The
FormatterRegistry SPI allows you to configure Formatting rules
centrally, instead of duplicating such configuration across your
Controllers. For example, you might want to enforce that all Date fields
are formatted a certain way, or fields with a specific annotation are
formatted in a certain way. With a shared FormatterRegistry, you define
these rules once and they are applied whenever formatting is needed. </para>
<para> The FormatterRegistry is an SPI for registering formatters and
converters. <classname>FormattingConversionService</classname> is
an implementation of FormatterRegistry suitable for most environments.
This implementation may be configured programatically or declaratively
as a Spring bean using
<classname>FormattingConversionServiceFactoryBean</classname>.
Because this implemementation also implements
<classname>ConversionService</classname>, it can be directly
configured for use with Spring's DataBinder and the Spring Expression
Language (SpEL).
</para>
<para> Review the FormatterRegistry SPI below: </para>
<programlisting language="java"><![CDATA[package org.springframework.format;
public interface FormatterRegistry {
public interface FormatterRegistry extends ConverterRegistry {
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Formatter<?> formatter);
void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory);
}]]></programlisting>
<para> As shown above, Formatters can be registered by fieldType or
annotation. <classname>FormattingConversionService</classname> is the
implementation of <classname>FormatterRegistry</classname> suitable for
most environments. This implementation may be configured
programatically, or declaratively as a Spring bean using
<classname>FormattingConversionServiceFactoryBean</classname>. Because
this implemementation also implements
<classname>ConversionService</classname>, it can be directly configured
for use with Spring's DataBinder and the Spring Expression Language
(SpEL). </para>
annotation.
</para>
<para> The FormatterRegistry SPI allows you to configure Formatting rules
centrally, instead of duplicating such configuration across your
Controllers. For example, you might want to enforce that all Date fields
are formatted a certain way, or fields with a specific annotation are
formatted in a certain way. With a shared FormatterRegistry, you define
these rules once and they are applied whenever formatting is needed.
</para>
</section>
<section id="format-configuring-FormatterRegistry">
<section id="format-FormatterRegistrar-SPI">
<title>FormatterRegistrar SPI</title>
<para> The FormatterRegistrar is an SPI for registering formatters and
converters through the FormatterRegistry:
</para>
<programlisting language="java"><![CDATA[package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}]]></programlisting>
<para> A FormatterRegistrar is useful when registering multiple related
converters and formatters for a given formatting category, such as Date
formatting. It can also be useful where declarative registration is
insufficient. For example when a formatter needs to be indexed under a
specific field type different from its own &lt;T&gt; or when registering
a Printer/Parser pair. The next section provides more information on
converter and formatter registration.
</para>
</section>
<section id="format-configuring-FormattingConverionService">
<title>Configuring Formatting in Spring MVC</title>
<para> In a Spring MVC application, you may configure a custom
@ -1419,7 +1450,9 @@ public interface FormatterRegistry {
classpath. </para>
<para> To inject a ConversionService instance with custom formatters and
converters registered, set the conversion-service attribute: </para>
converters registered, set the conversion-service attribute and then
specify custom converters, formatters, or FormatterRegistrars as properties
of the FormattingConversionServiceFactoryBean: </para>
<programlisting language="xml"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
@ -1433,15 +1466,35 @@ public interface FormatterRegistry {
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean"/>
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="org.example.MyConverter"/>
</set>
</property>
<property name="formatters">
<set>
<bean class="org.example.MyFormatter"/>
<bean class="org.example.MyAnnotationFormatterFactory"/>
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.example.MyFormatterRegistrar"/>
</set>
</property>
</bean>
</beans>
]]></programlisting>
<para> A custom ConversionService instance is often constructed by a
FactoryBean that internally registers custom Formatters and Converters
programatically before the ConversionService is returned. See
FormattingConversionServiceFactoryBean for an example. </para>
<note>
<para> See <xref linkend="format-FormatterRegistrar-SPI"/> and
the <classname>FormattingConversionServiceFactoryBean</classname>
for more information on when to use FormatterRegistrars.
</para>
</note>
</section>
</section>