Browse Source

Add Printer and Parser beans to conversion service

Update `WebMvcAutoConfiguration` and `WebFluxAutoConfiguration` so that
`Printer<T>` and `Parser<T>` beans are automatically registered with
the conversion service.

Prior to this commit only `GenericConverter`, `Converter` and
`Formatter` beans were automatically registered.

See gh-17064
pull/17199/head
Dmytro Nosan 7 years ago committed by Phillip Webb
parent
commit
955eaa87ae
  1. 15
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java
  2. 14
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java
  3. 68
      spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java
  4. 66
      spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java
  5. 93
      spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ParserConverter.java
  6. 85
      spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/PrinterConverter.java
  7. 116
      spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ParserConverterTests.java
  8. 93
      spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/PrinterConverterTests.java

15
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java

@ -38,6 +38,8 @@ import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceCh @@ -38,6 +38,8 @@ import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceCh
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.convert.ParserConverter;
import org.springframework.boot.convert.PrinterConverter;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.boot.web.reactive.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.context.annotation.Bean;
@ -48,6 +50,8 @@ import org.springframework.core.convert.converter.Converter; @@ -48,6 +50,8 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.util.ClassUtils;
@ -185,6 +189,17 @@ public class WebFluxAutoConfiguration { @@ -185,6 +189,17 @@ public class WebFluxAutoConfiguration {
for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
registry.addFormatter(formatter);
}
for (Printer<?> printer : getBeansOfType(Printer.class)) {
if (!(printer instanceof Formatter<?>)) {
registry.addConverter(new PrinterConverter(printer));
}
}
for (Parser<?> parser : getBeansOfType(Parser.class)) {
if (!(parser instanceof Formatter<?>)) {
registry.addConverter(new ParserConverter(parser));
}
}
}
private <T> Collection<T> getBeansOfType(Class<T> type) {

14
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java

@ -55,6 +55,8 @@ import org.springframework.boot.autoconfigure.web.ResourceProperties; @@ -55,6 +55,8 @@ import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy;
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.convert.ParserConverter;
import org.springframework.boot.convert.PrinterConverter;
import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter;
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
@ -74,6 +76,8 @@ import org.springframework.core.io.ResourceLoader; @@ -74,6 +76,8 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
@ -307,6 +311,16 @@ public class WebMvcAutoConfiguration { @@ -307,6 +311,16 @@ public class WebMvcAutoConfiguration {
for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
registry.addFormatter(formatter);
}
for (Printer<?> printer : getBeansOfType(Printer.class)) {
if (!(printer instanceof Formatter<?>)) {
registry.addConverter(new PrinterConverter(printer));
}
}
for (Parser<?> parser : getBeansOfType(Parser.class)) {
if (!(parser instanceof Formatter<?>)) {
registry.addConverter(new ParserConverter(parser));
}
}
}
private <T> Collection<T> getBeansOfType(Class<T> type) {

68
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java

@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.web.reactive; @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.web.reactive;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -40,7 +41,10 @@ import org.springframework.context.annotation.Configuration; @@ -40,7 +41,10 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.ClassPathResource;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl;
import org.springframework.http.codec.ServerCodecConfigurer;
@ -373,6 +377,17 @@ class WebFluxAutoConfigurationTests { @@ -373,6 +377,17 @@ class WebFluxAutoConfigurationTests {
Assertions.setExtractBareNamePropertyMethods(true);
}
@Test
void customPrinterAndParserShouldBeRegisteredAsConverters() {
this.contextRunner.withUserConfiguration(ParserConfiguration.class, PrinterConfiguration.class)
.run((context) -> {
Foo foo = new Foo("bar");
ConversionService conversionService = context.getBean(ConversionService.class);
assertThat(conversionService.convert(foo, String.class)).isEqualTo("bar");
assertThat(conversionService.convert("bar", Foo.class)).extracting(Foo::toString).isEqualTo("bar");
});
}
private Map<PathPattern, Object> getHandlerMap(ApplicationContext context) {
HandlerMapping mapping = context.getBean("resourceHandlerMapping", HandlerMapping.class);
if (mapping instanceof SimpleUrlHandlerMapping) {
@ -545,4 +560,57 @@ class WebFluxAutoConfigurationTests { @@ -545,4 +560,57 @@ class WebFluxAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class PrinterConfiguration {
@Bean
public Printer<Foo> fooPrinter() {
return new FooPrinter();
}
private static class FooPrinter implements Printer<Foo> {
@Override
public String print(Foo foo, Locale locale) {
return foo.toString();
}
}
}
@Configuration(proxyBeanMethods = false)
static class ParserConfiguration {
@Bean
public Parser<Foo> fooParser() {
return new FooParser();
}
private static class FooParser implements Parser<Foo> {
@Override
public Foo parse(String source, Locale locale) {
return new Foo(source);
}
}
}
static class Foo {
private final String name;
Foo(String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
}
}

66
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java

@ -56,6 +56,8 @@ import org.springframework.core.convert.ConversionService; @@ -56,6 +56,8 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
@ -773,6 +775,17 @@ class WebMvcAutoConfigurationTests { @@ -773,6 +775,17 @@ class WebMvcAutoConfigurationTests {
});
}
@Test
void customPrinterAndParserShouldBeRegisteredAsConverters() {
this.contextRunner.withUserConfiguration(ParserConfiguration.class, PrinterConfiguration.class)
.run((context) -> {
Foo foo = new Foo("bar");
ConversionService conversionService = context.getBean(ConversionService.class);
assertThat(conversionService.convert(foo, String.class)).isEqualTo("bar");
assertThat(conversionService.convert("bar", Foo.class)).extracting(Foo::toString).isEqualTo("bar");
});
}
private void assertCacheControl(AssertableWebApplicationContext context) {
Map<String, Object> handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class));
assertThat(handlerMap).hasSize(2);
@ -1093,4 +1106,57 @@ class WebMvcAutoConfigurationTests { @@ -1093,4 +1106,57 @@ class WebMvcAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class PrinterConfiguration {
@Bean
public Printer<Foo> fooPrinter() {
return new FooPrinter();
}
private static class FooPrinter implements Printer<Foo> {
@Override
public String print(Foo foo, Locale locale) {
return foo.toString();
}
}
}
@Configuration(proxyBeanMethods = false)
static class ParserConfiguration {
@Bean
public Parser<Foo> fooParser() {
return new FooParser();
}
private static class FooParser implements Parser<Foo> {
@Override
public Foo parse(String source, Locale locale) {
return new Foo(source);
}
}
}
static class Foo {
private final String name;
Foo(String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
}
}

93
spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ParserConverter.java

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
/*
* Copyright 2012-2019 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
*
* https://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.boot.convert;
import java.text.ParseException;
import java.util.Collections;
import java.util.Set;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.format.Parser;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link Converter} to convert from a {@link String} to {@code <T>} using the underlying
* {@link Parser}{@code <T>}.
*
* @author Dmytro Nosan
* @since 2.2.0
*/
public class ParserConverter implements GenericConverter {
private final Class<?> type;
private final Parser<?> parser;
/**
* Creates a {@code Converter} to convert {@code String} to a {@code T} via parser.
* @param parser parses {@code String} to a {@code T}
*/
public ParserConverter(Parser<?> parser) {
Assert.notNull(parser, "Parser must not be null");
this.type = getType(parser);
this.parser = parser;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, this.type));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
String value = (String) source;
if (!StringUtils.hasText(value)) {
return null;
}
try {
return this.parser.parse(value, LocaleContextHolder.getLocale());
}
catch (ParseException ex) {
throw new IllegalArgumentException("Value [" + value + "] can not be parsed", ex);
}
}
@Override
public String toString() {
return String.class.getName() + " -> " + this.type.getName() + " : " + this.parser;
}
private static Class<?> getType(Parser<?> parser) {
Class<?> type = GenericTypeResolver.resolveTypeArgument(parser.getClass(), Parser.class);
if (type == null && parser instanceof DecoratingProxy) {
type = GenericTypeResolver.resolveTypeArgument(((DecoratingProxy) parser).getDecoratedClass(),
Parser.class);
}
if (type == null) {
throw new IllegalArgumentException("Unable to extract the parameterized type from Parser: '"
+ parser.getClass().getName() + "'. Does the class parameterize the <T> generic type?");
}
return type;
}
}

85
spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/PrinterConverter.java

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
/*
* Copyright 2012-2019 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
*
* https://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.boot.convert;
import java.util.Collections;
import java.util.Set;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.format.Printer;
import org.springframework.util.Assert;
/**
* {@link Converter} to convert {@code <T>} to a {@link String} using the underlying
* {@link Printer}{@code <T>}.
*
* @author Dmytro Nosan
* @since 2.2.0
*/
public class PrinterConverter implements GenericConverter {
private final Printer printer;
private final Class<?> type;
/**
* Creates a {@code Converter} to convert {@code T} to a {@code String} via printer.
* @param printer prints {@code T} to a {@code String}
*/
public PrinterConverter(Printer<?> printer) {
Assert.notNull(printer, "Printer must not be null");
this.type = getType(printer);
this.printer = printer;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(this.type, String.class));
}
@Override
@SuppressWarnings("unchecked")
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return "";
}
return this.printer.print(source, LocaleContextHolder.getLocale());
}
public String toString() {
return this.type.getName() + " -> " + String.class.getName() + " : " + this.printer;
}
private static Class<?> getType(Printer<?> printer) {
Class<?> type = GenericTypeResolver.resolveTypeArgument(printer.getClass(), Printer.class);
if (type == null && printer instanceof DecoratingProxy) {
type = GenericTypeResolver.resolveTypeArgument(((DecoratingProxy) printer).getDecoratedClass(),
Printer.class);
}
if (type == null) {
throw new IllegalArgumentException("Unable to extract the parameterized type from Printer: '"
+ printer.getClass().getName() + "'. Does the class parameterize the <T> generic type?");
}
return type;
}
}

116
spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ParserConverterTests.java

@ -0,0 +1,116 @@ @@ -0,0 +1,116 @@
/*
* Copyright 2012-2019 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
*
* https://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.boot.convert;
import java.text.ParseException;
import java.time.Duration;
import java.util.Locale;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.Parser;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ParserConverter}.
*
* @author Dmytro Nosan
*/
class ParserConverterTests {
private final DefaultConversionService conversionService = new DefaultConversionService();
@BeforeEach
void addParsers() {
this.conversionService.addConverter(new ParserConverter(new DurationParser()));
this.conversionService
.addConverter(new ParserConverter(((Parser<?>) new ProxyFactory(new DataSizeParser()).getProxy())));
}
@Test
void convertStringToDataSize() {
assertThat(convert("1KB", DataSize.class)).isEqualTo(DataSize.ofKilobytes(1));
assertThat(convert("", DataSize.class)).isNull();
assertThat(convert(null, DataSize.class)).isNull();
}
@Test
void convertStringToDuration() {
assertThat(convert("PT1S", Duration.class)).isEqualTo(Duration.ofSeconds(1));
assertThat(convert(null, Duration.class)).isNull();
assertThat(convert("", Duration.class)).isNull();
}
@Test
void shouldFailParserGenericCanNotBeResolved() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.conversionService.addConverter(new ParserConverter((source, locale) -> "")))
.withMessageContaining("Unable to extract the parameterized type from Parser");
}
@Test
void shouldFailParserThrowsParserException() {
this.conversionService.addConverter(new ParserConverter(new ObjectParser()));
assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> convert("Text", Object.class))
.withCauseInstanceOf(IllegalArgumentException.class)
.withMessageContaining("Value [Text] can not be parsed");
}
private <T> T convert(String source, Class<T> type) {
return type.cast(this.conversionService.convert(source, TypeDescriptor.valueOf(String.class),
TypeDescriptor.valueOf(type)));
}
private static class DataSizeParser implements Parser<DataSize> {
@Override
public DataSize parse(String value, Locale locale) {
return DataSize.parse(value);
}
}
private static class DurationParser implements Parser<Duration> {
@Override
public Duration parse(String value, Locale locale) {
return Duration.parse(value);
}
}
private static class ObjectParser implements Parser<Object> {
@Override
public Object parse(String source, Locale locale) throws ParseException {
throw new ParseException("", 0);
}
}
}

93
spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/PrinterConverterTests.java

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
/*
* Copyright 2012-2019 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
*
* https://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.boot.convert;
import java.time.Duration;
import java.util.Locale;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.Printer;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link PrinterConverter}.
*
* @author Dmytro Nosan
*/
class PrinterConverterTests {
private final DefaultConversionService conversionService = new DefaultConversionService();
@BeforeEach
void addPrinters() {
this.conversionService.addConverter(new PrinterConverter(new DurationPrinter()));
this.conversionService
.addConverter(new PrinterConverter(((Printer<?>) new ProxyFactory(new DataSizePrinter()).getProxy())));
}
@Test
void convertDataSizeToString() {
assertThat(convert(DataSize.ofKilobytes(1), DataSize.class)).isEqualTo("1024B");
assertThat(convert(null, DataSize.class)).isEmpty();
}
@Test
void convertDurationToString() {
assertThat(convert(Duration.ofSeconds(1), Duration.class)).isEqualTo("PT1S");
assertThat(convert(null, Duration.class)).isEmpty();
}
@Test
void shouldFailPrinterGenericCanNotBeResolved() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.conversionService.addConverter(new PrinterConverter((source, locale) -> "")))
.withMessageContaining("Unable to extract the parameterized type from Printer");
}
private <T> String convert(T source, Class<T> type) {
return (String) this.conversionService.convert(source, TypeDescriptor.valueOf(type),
TypeDescriptor.valueOf(String.class));
}
private static class DataSizePrinter implements Printer<DataSize> {
@Override
public String print(DataSize dataSize, Locale locale) {
return dataSize.toString();
}
}
private static class DurationPrinter implements Printer<Duration> {
@Override
public String print(Duration duration, Locale locale) {
return duration.toString();
}
}
}
Loading…
Cancel
Save