Skip to content

Commit 0fb801a

Browse files
committed
Add Printer<T> and Parser<T> to FormatterRegistry if they have been exposed as beans.
see gh-16171
1 parent 75e45fd commit 0fb801a

File tree

8 files changed

+550
-0
lines changed

8 files changed

+550
-0
lines changed

Diff for: spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java

+15
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import org.springframework.boot.autoconfigure.web.ResourceProperties;
3939
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
4040
import org.springframework.boot.context.properties.EnableConfigurationProperties;
41+
import org.springframework.boot.convert.ParserConverter;
42+
import org.springframework.boot.convert.PrinterConverter;
4143
import org.springframework.boot.web.codec.CodecCustomizer;
4244
import org.springframework.boot.web.reactive.filter.OrderedHiddenHttpMethodFilter;
4345
import org.springframework.context.annotation.Bean;
@@ -48,6 +50,8 @@
4850
import org.springframework.core.convert.converter.GenericConverter;
4951
import org.springframework.format.Formatter;
5052
import org.springframework.format.FormatterRegistry;
53+
import org.springframework.format.Parser;
54+
import org.springframework.format.Printer;
5155
import org.springframework.format.support.FormattingConversionService;
5256
import org.springframework.http.codec.ServerCodecConfigurer;
5357
import org.springframework.util.ClassUtils;
@@ -185,6 +189,17 @@ public void addFormatters(FormatterRegistry registry) {
185189
for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
186190
registry.addFormatter(formatter);
187191
}
192+
for (Printer<?> printer : getBeansOfType(Printer.class)) {
193+
if (!(printer instanceof Formatter<?>)) {
194+
registry.addConverter(new PrinterConverter(printer));
195+
196+
}
197+
}
198+
for (Parser<?> parser : getBeansOfType(Parser.class)) {
199+
if (!(parser instanceof Formatter<?>)) {
200+
registry.addConverter(new ParserConverter(parser));
201+
}
202+
}
188203
}
189204

190205
private <T> Collection<T> getBeansOfType(Class<T> type) {

Diff for: spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java

+14
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy;
5656
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
5757
import org.springframework.boot.context.properties.EnableConfigurationProperties;
58+
import org.springframework.boot.convert.ParserConverter;
59+
import org.springframework.boot.convert.PrinterConverter;
5860
import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter;
5961
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
6062
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
@@ -74,6 +76,8 @@
7476
import org.springframework.core.task.AsyncTaskExecutor;
7577
import org.springframework.format.Formatter;
7678
import org.springframework.format.FormatterRegistry;
79+
import org.springframework.format.Parser;
80+
import org.springframework.format.Printer;
7781
import org.springframework.format.support.FormattingConversionService;
7882
import org.springframework.http.CacheControl;
7983
import org.springframework.http.MediaType;
@@ -307,6 +311,16 @@ public void addFormatters(FormatterRegistry registry) {
307311
for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
308312
registry.addFormatter(formatter);
309313
}
314+
for (Printer<?> printer : getBeansOfType(Printer.class)) {
315+
if (!(printer instanceof Formatter<?>)) {
316+
registry.addConverter(new PrinterConverter(printer));
317+
}
318+
}
319+
for (Parser<?> parser : getBeansOfType(Parser.class)) {
320+
if (!(parser instanceof Formatter<?>)) {
321+
registry.addConverter(new ParserConverter(parser));
322+
}
323+
}
310324
}
311325

312326
private <T> Collection<T> getBeansOfType(Class<T> type) {

Diff for: spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java

+68
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Collections;
2020
import java.util.Date;
2121
import java.util.List;
22+
import java.util.Locale;
2223
import java.util.Map;
2324
import java.util.concurrent.TimeUnit;
2425

@@ -40,7 +41,10 @@
4041
import org.springframework.context.annotation.Import;
4142
import org.springframework.core.Ordered;
4243
import org.springframework.core.annotation.Order;
44+
import org.springframework.core.convert.ConversionService;
4345
import org.springframework.core.io.ClassPathResource;
46+
import org.springframework.format.Parser;
47+
import org.springframework.format.Printer;
4448
import org.springframework.format.support.FormattingConversionService;
4549
import org.springframework.http.CacheControl;
4650
import org.springframework.http.codec.ServerCodecConfigurer;
@@ -373,6 +377,17 @@ void cacheControl() {
373377
Assertions.setExtractBareNamePropertyMethods(true);
374378
}
375379

380+
@Test
381+
void customPrinterAndParserShouldBeRegisteredAsConverters() {
382+
this.contextRunner.withUserConfiguration(ParserConfiguration.class, PrinterConfiguration.class)
383+
.run((context) -> {
384+
Foo foo = new Foo("bar");
385+
ConversionService conversionService = context.getBean(ConversionService.class);
386+
assertThat(conversionService.convert(foo, String.class)).isEqualTo("bar");
387+
assertThat(conversionService.convert("bar", Foo.class)).extracting(Foo::toString).isEqualTo("bar");
388+
});
389+
}
390+
376391
private Map<PathPattern, Object> getHandlerMap(ApplicationContext context) {
377392
HandlerMapping mapping = context.getBean("resourceHandlerMapping", HandlerMapping.class);
378393
if (mapping instanceof SimpleUrlHandlerMapping) {
@@ -545,4 +560,57 @@ private static class MyRequestMappingHandlerMapping extends RequestMappingHandle
545560

546561
}
547562

563+
@Configuration(proxyBeanMethods = false)
564+
static class PrinterConfiguration {
565+
566+
@Bean
567+
public Printer<Foo> fooPrinter() {
568+
return new FooPrinter();
569+
}
570+
571+
private static class FooPrinter implements Printer<Foo> {
572+
573+
@Override
574+
public String print(Foo foo, Locale locale) {
575+
return foo.toString();
576+
}
577+
578+
}
579+
580+
}
581+
582+
@Configuration(proxyBeanMethods = false)
583+
static class ParserConfiguration {
584+
585+
@Bean
586+
public Parser<Foo> fooParser() {
587+
return new FooParser();
588+
}
589+
590+
private static class FooParser implements Parser<Foo> {
591+
592+
@Override
593+
public Foo parse(String source, Locale locale) {
594+
return new Foo(source);
595+
}
596+
597+
}
598+
599+
}
600+
601+
static class Foo {
602+
603+
private final String name;
604+
605+
Foo(String name) {
606+
this.name = name;
607+
}
608+
609+
@Override
610+
public String toString() {
611+
return this.name;
612+
}
613+
614+
}
615+
548616
}

Diff for: spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java

+66
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
import org.springframework.core.io.ClassPathResource;
5757
import org.springframework.core.io.Resource;
5858
import org.springframework.core.task.AsyncTaskExecutor;
59+
import org.springframework.format.Parser;
60+
import org.springframework.format.Printer;
5961
import org.springframework.format.support.FormattingConversionService;
6062
import org.springframework.http.CacheControl;
6163
import org.springframework.http.HttpHeaders;
@@ -773,6 +775,17 @@ void whenUserDefinesARequestContextFilterRegistrationTheAutoConfiguredFilterBack
773775
});
774776
}
775777

778+
@Test
779+
void customPrinterAndParserShouldBeRegisteredAsConverters() {
780+
this.contextRunner.withUserConfiguration(ParserConfiguration.class, PrinterConfiguration.class)
781+
.run((context) -> {
782+
Foo foo = new Foo("bar");
783+
ConversionService conversionService = context.getBean(ConversionService.class);
784+
assertThat(conversionService.convert(foo, String.class)).isEqualTo("bar");
785+
assertThat(conversionService.convert("bar", Foo.class)).extracting(Foo::toString).isEqualTo("bar");
786+
});
787+
}
788+
776789
private void assertCacheControl(AssertableWebApplicationContext context) {
777790
Map<String, Object> handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class));
778791
assertThat(handlerMap).hasSize(2);
@@ -1093,4 +1106,57 @@ public FilterRegistrationBean<RequestContextFilter> customRequestContextFilterRe
10931106

10941107
}
10951108

1109+
@Configuration(proxyBeanMethods = false)
1110+
static class PrinterConfiguration {
1111+
1112+
@Bean
1113+
public Printer<Foo> fooPrinter() {
1114+
return new FooPrinter();
1115+
}
1116+
1117+
private static class FooPrinter implements Printer<Foo> {
1118+
1119+
@Override
1120+
public String print(Foo foo, Locale locale) {
1121+
return foo.toString();
1122+
}
1123+
1124+
}
1125+
1126+
}
1127+
1128+
@Configuration(proxyBeanMethods = false)
1129+
static class ParserConfiguration {
1130+
1131+
@Bean
1132+
public Parser<Foo> fooParser() {
1133+
return new FooParser();
1134+
}
1135+
1136+
private static class FooParser implements Parser<Foo> {
1137+
1138+
@Override
1139+
public Foo parse(String source, Locale locale) {
1140+
return new Foo(source);
1141+
}
1142+
1143+
}
1144+
1145+
}
1146+
1147+
static class Foo {
1148+
1149+
private final String name;
1150+
1151+
Foo(String name) {
1152+
this.name = name;
1153+
}
1154+
1155+
@Override
1156+
public String toString() {
1157+
return this.name;
1158+
}
1159+
1160+
}
1161+
10961162
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.convert;
18+
19+
import java.text.ParseException;
20+
import java.util.Collections;
21+
import java.util.Set;
22+
23+
import org.springframework.context.i18n.LocaleContextHolder;
24+
import org.springframework.core.DecoratingProxy;
25+
import org.springframework.core.GenericTypeResolver;
26+
import org.springframework.core.convert.TypeDescriptor;
27+
import org.springframework.core.convert.converter.Converter;
28+
import org.springframework.core.convert.converter.GenericConverter;
29+
import org.springframework.format.Parser;
30+
import org.springframework.util.Assert;
31+
import org.springframework.util.StringUtils;
32+
33+
/**
34+
* {@link Converter} to convert from a {@link String} to {@code <T>} using the underlying
35+
* {@link Parser}{@code <T>}.
36+
*
37+
* @author Dmytro Nosan
38+
* @since 2.2.0
39+
*/
40+
public class ParserConverter implements GenericConverter {
41+
42+
private final Class<?> type;
43+
44+
private final Parser<?> parser;
45+
46+
/**
47+
* Creates a {@code Converter} to convert {@code String} to a {@code T} via parser.
48+
* @param parser parses {@code String} to a {@code T}
49+
*/
50+
public ParserConverter(Parser<?> parser) {
51+
Assert.notNull(parser, "Parser must not be null");
52+
this.type = getType(parser);
53+
this.parser = parser;
54+
}
55+
56+
@Override
57+
public Set<ConvertiblePair> getConvertibleTypes() {
58+
return Collections.singleton(new ConvertiblePair(String.class, this.type));
59+
}
60+
61+
@Override
62+
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
63+
String value = (String) source;
64+
if (!StringUtils.hasText(value)) {
65+
return null;
66+
}
67+
try {
68+
return this.parser.parse(value, LocaleContextHolder.getLocale());
69+
}
70+
catch (ParseException ex) {
71+
throw new IllegalArgumentException("Value [" + value + "] can not be parsed", ex);
72+
}
73+
}
74+
75+
@Override
76+
public String toString() {
77+
return String.class.getName() + " -> " + this.type.getName() + " : " + this.parser;
78+
}
79+
80+
private static Class<?> getType(Parser<?> parser) {
81+
Class<?> type = GenericTypeResolver.resolveTypeArgument(parser.getClass(), Parser.class);
82+
if (type == null && parser instanceof DecoratingProxy) {
83+
type = GenericTypeResolver.resolveTypeArgument(((DecoratingProxy) parser).getDecoratedClass(),
84+
Parser.class);
85+
}
86+
if (type == null) {
87+
throw new IllegalArgumentException("Unable to extract the parameterized type from Parser: '"
88+
+ parser.getClass().getName() + "'. Does the class parameterize the <T> generic type?");
89+
}
90+
return type;
91+
}
92+
93+
}

0 commit comments

Comments
 (0)