Browse Source
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-17064pull/17199/head
8 changed files with 550 additions and 0 deletions
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -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…
Reference in new issue