From abff2b959bf4cce52d4821ae97d66612b4ac6016 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 27 Jan 2011 11:26:19 +0000 Subject: [PATCH] SPR-7912 Add tests for FormattingConversionServiceFactoryBean, update reference docs, and remove mvc:formatters --- ...tingConversionServiceFactoryBeanTests.java | 201 ++++++++++++++++++ .../web/servlet/config/spring-mvc-3.1.xsd | 19 -- ...tationDrivenBeanDefinitionParserTests.java | 131 ------------ .../config/mvc-config-annotation-driven.xml | 4 - spring-framework-reference/src/mvc.xml | 76 +++++-- .../src/spring-framework-reference.xml | 10 +- spring-framework-reference/src/validation.xml | 101 ++++++--- 7 files changed, 350 insertions(+), 192 deletions(-) create mode 100644 org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java diff --git a/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java b/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java new file mode 100644 index 00000000000..6fd400dc55d --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java @@ -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 formatters = new HashSet(); + 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 registrars = new HashSet(); + 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 formatters = new HashSet(); + 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 { + + 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 { + + private final Set> fieldTypes = new HashSet>(1); + + public SpecialIntAnnotationFormatterFactory() { + fieldTypes.add(Integer.class); + } + + public Set> getFieldTypes() { + return fieldTypes; + } + + public Printer getPrinter(SpecialInt annotation, Class fieldType) { + return new Printer() { + public String print(Integer object, Locale locale) { + return ":" + object.toString(); + } + }; + } + + public Parser getParser(SpecialInt annotation, Class fieldType) { + return new Parser() { + 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()); + } + + } + +} diff --git a/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd b/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd index c65528db118..e62fb75a895 100644 --- a/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd +++ b/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd @@ -38,25 +38,6 @@ - - - - - - - - - - - - - - diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java index dfc9accb5d5..04a57ae43e4 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java @@ -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 { - - 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 { - - private final Set> fieldTypes = new HashSet>(1); - - @SuppressWarnings("unused") - public TestBeanAnnotationFormatterFactory() { - fieldTypes.add(Integer.class); - } - - public Set> getFieldTypes() { - return fieldTypes; - } - - public Printer getPrinter(SpecialIntFormat annotation, Class fieldType) { - return new Printer() { - public String print(Integer object, Locale locale) { - return ">>" + object.toString() + "<<"; - } - }; - } - - public Parser getParser(SpecialIntFormat annotation, Class fieldType) { - return new Parser() { - 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 { - } - } diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-annotation-driven.xml b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-annotation-driven.xml index fc7e450b24c..6a6d09fc61b 100644 --- a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-annotation-driven.xml +++ b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-annotation-driven.xml @@ -10,10 +10,6 @@ - - - - AnnotationMethodHandlerAdapter is extended to support the @RequestBody and has the following HttpMessageConverters - registered by default: + registered by default if not using the MVC namespace: @@ -1301,16 +1301,12 @@ public void handle(@RequestBody String body, Writer writer) throws IOException { SourceHttpMessageConverter converts to/from a javax.xml.transform.Source. - - - MarshallingHttpMessageConverter - converts to/from an object using the - org.springframework.oxm package. - For more information on these converters, see Message Converters. + linkend="rest-message-conversion">Message Converters. + Also see for information + on the default message converters set up by the MVC namespace. The MarshallingHttpMessageConverter requires a Marshaller and @@ -3313,13 +3309,69 @@ public class SimpleController { - Support for reading and writing XML, if JAXB is present on the classpath. + HttpMessageConverter support for @RequestBody method parameters and @ResponseBody method return values. - - - 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: + + + ByteArrayHttpMessageConverter + converts byte arrays. + + + + StringHttpMessageConverter converts + strings. + + + + ResourceHttpMessageConverter converts + to/from org.springframework.core.io.Resource + for all media types. + + + + SourceHttpMessageConverter converts + to/from a javax.xml.transform.Source. + + + + FormHttpMessageConverter converts + form data to/from a MultiValueMap<String, String>. + + + + Jaxb2RootElementHttpMessageConverter + converts Java objects to/from XML -- added if JAXB2 is present + on the classpath. + + + + + MappingJacksonHttpMessageConverter + converts to/from JSON -- added if Jackson is present on the classpath. + + + + + AtomFeedHttpMessageConverter + converts Atom feeds -- added if Rome is present on the classpath. + + + + + RssChannelHttpMessageConverter + converts RSS feeds -- added if Rome is present on the classpath. + + + + + + This list of HttpMessageConverters used can be replaced through + the mvc:message-converters sub-element of mvc:annotation-driven. + + A typical usage is shown below: diff --git a/spring-framework-reference/src/spring-framework-reference.xml b/spring-framework-reference/src/spring-framework-reference.xml index 3fdf66a5285..286bf04998e 100644 --- a/spring-framework-reference/src/spring-framework-reference.xml +++ b/spring-framework-reference/src/spring-framework-reference.xml @@ -172,17 +172,23 @@ Gierke + + + Rossen + Stoyanchev + + - 2004-2010 + 2004-2011 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 + Andy Clement, Dave Syer, Oliver Gierke, Rossen Stoyanchev diff --git a/spring-framework-reference/src/validation.xml b/spring-framework-reference/src/validation.xml index af7ed571854..a0eee19ad1b 100644 --- a/spring-framework-reference/src/validation.xml +++ b/spring-framework-reference/src/validation.xml @@ -1350,41 +1350,72 @@ public interface AnnotationFormatterFactory {
FormatterRegistry SPI - 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. + The FormatterRegistry is an SPI for registering formatters and + converters. FormattingConversionService is + an implementation of FormatterRegistry suitable for most environments. + This implementation may be configured programatically or declaratively + as a Spring bean using + FormattingConversionServiceFactoryBean. + Because this implemementation also implements + ConversionService, it can be directly + configured for use with Spring's DataBinder and the Spring Expression + Language (SpEL). + Review the FormatterRegistry SPI below: fieldType, Printer printer, Parser parser); void addFormatterForFieldType(Class fieldType, Formatter formatter); + void addFormatterForFieldType(Formatter formatter); + void addFormatterForAnnotation(AnnotationFormatterFactory factory); }]]> As shown above, Formatters can be registered by fieldType or - annotation. FormattingConversionService is the - implementation of FormatterRegistry suitable for - most environments. This implementation may be configured - programatically, or declaratively as a Spring bean using - FormattingConversionServiceFactoryBean. Because - this implemementation also implements - ConversionService, it can be directly configured - for use with Spring's DataBinder and the Spring Expression Language - (SpEL). + annotation. + + 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. +
-
+
+ FormatterRegistrar SPI + + The FormatterRegistrar is an SPI for registering formatters and + converters through the FormatterRegistry: + + + + + 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 <T> or when registering + a Printer/Parser pair. The next section provides more information on + converter and formatter registration. + +
+ +
Configuring Formatting in Spring MVC In a Spring MVC application, you may configure a custom @@ -1419,7 +1450,9 @@ public interface FormatterRegistry { classpath. To inject a ConversionService instance with custom formatters and - converters registered, set the conversion-service attribute: + converters registered, set the conversion-service attribute and then + specify custom converters, formatters, or FormatterRegistrars as properties + of the FormattingConversionServiceFactoryBean: + class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> + + + + + + + + + + + + + + + + + ]]> - 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. + + See and + the FormattingConversionServiceFactoryBean + for more information on when to use FormatterRegistrars. + + +