diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java index ff4299dc03..ffbb3a870b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java @@ -41,7 +41,7 @@ class ArrayBinder extends IndexedElementsBinder { IndexedCollectionSupplier result = new IndexedCollectionSupplier(ArrayList::new); ResolvableType aggregateType = target.getType(); ResolvableType elementType = target.getType().getComponentType(); - bindIndexed(name, elementBinder, aggregateType, elementType, result); + bindIndexed(name, target, elementBinder, aggregateType, elementType, result); if (result.wasSupplied()) { List list = (List) result.get(); Object array = Array.newInstance(elementType.resolve(), list.size()); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java index b89b6c2e2c..bb9a889384 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java @@ -45,7 +45,7 @@ class CollectionBinder extends IndexedElementsBinder> { ResolvableType elementType = target.getType().asCollection().getGeneric(); IndexedCollectionSupplier result = new IndexedCollectionSupplier( () -> CollectionFactory.createCollection(collectionType, 0)); - bindIndexed(name, elementBinder, aggregateType, elementType, result); + bindIndexed(name, target, elementBinder, aggregateType, elementType, result); if (result.wasSupplied()) { return result.get(); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java index 0c9901d882..f441239f70 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java @@ -16,6 +16,7 @@ package org.springframework.boot.context.properties.bind; +import java.lang.annotation.Annotation; import java.util.Collection; import java.util.List; import java.util.TreeSet; @@ -56,16 +57,18 @@ abstract class IndexedElementsBinder extends AggregateBinder { /** * Bind indexed elements to the supplied collection. * @param name The name of the property to bind + * @param target the target bindable * @param elementBinder the binder to use for elements * @param aggregateType the aggregate type, may be a collection or an array * @param elementType the element type * @param result the destination for results */ - protected final void bindIndexed(ConfigurationPropertyName name, + protected final void bindIndexed(ConfigurationPropertyName name, Bindable target, AggregateElementBinder elementBinder, ResolvableType aggregateType, ResolvableType elementType, IndexedCollectionSupplier result) { for (ConfigurationPropertySource source : getContext().getSources()) { - bindIndexed(source, name, elementBinder, result, aggregateType, elementType); + bindIndexed(source, name, target, elementBinder, result, aggregateType, + elementType); if (result.wasSupplied() && result.get() != null) { return; } @@ -73,12 +76,13 @@ abstract class IndexedElementsBinder extends AggregateBinder { } private void bindIndexed(ConfigurationPropertySource source, - ConfigurationPropertyName root, AggregateElementBinder elementBinder, - IndexedCollectionSupplier collection, ResolvableType aggregateType, - ResolvableType elementType) { + ConfigurationPropertyName root, Bindable target, + AggregateElementBinder elementBinder, IndexedCollectionSupplier collection, + ResolvableType aggregateType, ResolvableType elementType) { ConfigurationProperty property = source.getConfigurationProperty(root); if (property != null) { - Object aggregate = convert(property.getValue(), aggregateType); + Object aggregate = convert(property.getValue(), aggregateType, + target.getAnnotations()); ResolvableType collectionType = forClassWithGenerics( collection.get().getClass(), elementType); Collection elements = convert(aggregate, collectionType); @@ -134,10 +138,11 @@ abstract class IndexedElementsBinder extends AggregateBinder { } } - private C convert(Object value, ResolvableType type) { + private C convert(Object value, ResolvableType type, Annotation... annotations) { value = getContext().getPlaceholdersResolver().resolvePlaceholders(value); BinderConversionService conversionService = getContext().getConversionService(); - return ResolvableTypeDescriptor.forType(type).convert(conversionService, value); + return ResolvableTypeDescriptor.forType(type, annotations) + .convert(conversionService, value); } // Work around for SPR-16456 diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java index dec6824076..34e82c0244 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -16,14 +16,17 @@ package org.springframework.boot.context.properties.bind.convert; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.function.Function; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.datetime.DateFormatter; import org.springframework.format.datetime.DateFormatterRegistrar; @@ -38,83 +41,88 @@ import org.springframework.format.support.DefaultFormattingConversionService; */ public class BinderConversionService implements ConversionService { - private static final ConversionService additionalConversionService = createAdditionalConversionService(); - private static final ConversionService defaultConversionService = new DefaultFormattingConversionService(); - private final ConversionService conversionService; + private final List conversionServices; /** * Create a new {@link BinderConversionService} instance. * @param conversionService and option root conversion service */ public BinderConversionService(ConversionService conversionService) { - this.conversionService = (conversionService != null ? conversionService - : defaultConversionService); + List conversionServices = new ArrayList<>(); + conversionServices.add(createOverrideConversionService()); + conversionServices.add( + conversionService != null ? conversionService : defaultConversionService); + conversionServices.add(createAdditionalConversionService()); + this.conversionServices = Collections.unmodifiableList(conversionServices); + } + + private ConversionService createOverrideConversionService() { + GenericConversionService service = new GenericConversionService(); + service.addConverter(new DelimitedStringToCollectionConverter(this)); + return service; + } + + private ConversionService createAdditionalConversionService() { + DefaultFormattingConversionService service = new DefaultFormattingConversionService(); + DefaultConversionService.addCollectionConverters(service); + service.addConverterFactory(new StringToEnumConverterFactory()); + service.addConverter(new StringToCharArrayConverter()); + service.addConverter(new StringToInetAddressConverter()); + service.addConverter(new InetAddressToStringConverter()); + service.addConverter(new PropertyEditorConverter()); + service.addConverter(new DurationConverter()); + DateFormatterRegistrar registrar = new DateFormatterRegistrar(); + DateFormatter formatter = new DateFormatter(); + formatter.setIso(DateTimeFormat.ISO.DATE_TIME); + registrar.setFormatter(formatter); + registrar.registerFormatters(service); + return service; } @Override public boolean canConvert(Class sourceType, Class targetType) { - return (this.conversionService != null - && this.conversionService.canConvert(sourceType, targetType)) - || additionalConversionService.canConvert(sourceType, targetType); + for (ConversionService service : this.conversionServices) { + if (service.canConvert(sourceType, targetType)) { + return true; + } + } + return false; } @Override public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { - return (this.conversionService != null - && this.conversionService.canConvert(sourceType, targetType)) - || additionalConversionService.canConvert(sourceType, targetType); + for (ConversionService service : this.conversionServices) { + if (service.canConvert(sourceType, targetType)) { + return true; + } + } + return false; } @Override public T convert(Object source, Class targetType) { - return callConversionService((c) -> c.convert(source, targetType)); + return callConversionServices((c) -> c.convert(source, targetType)); } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - return callConversionService((c) -> c.convert(source, sourceType, targetType)); + return callConversionServices((c) -> c.convert(source, sourceType, targetType)); } - private T callConversionService(Function call) { - if (this.conversionService == null) { - return callAdditionalConversionService(call, null); + private T callConversionServices(Function call) { + ConversionException exception = null; + for (ConversionService service : this.conversionServices) { + try { + return call.apply(service); + } + catch (ConversionException ex) { + exception = ex; + } } - try { - return call.apply(this.conversionService); - } - catch (ConversionException ex) { - return callAdditionalConversionService(call, ex); - } - } - - private T callAdditionalConversionService(Function call, - RuntimeException cause) { - try { - return call.apply(additionalConversionService); - } - catch (ConverterNotFoundException ex) { - throw (cause != null ? cause : ex); - } - } - - private static ConversionService createAdditionalConversionService() { - DefaultFormattingConversionService service = new DefaultFormattingConversionService(); - DefaultConversionService.addCollectionConverters(service); - service.addConverterFactory(new StringToEnumConverterFactory()); - service.addConverter(new StringToCharArrayConverter()); - service.addConverter(new StringToInetAddressConverter()); - service.addConverter(new InetAddressToStringConverter()); - service.addConverter(new PropertyEditorConverter()); - service.addConverter(new DurationConverter()); - DateFormatterRegistrar registrar = new DateFormatterRegistrar(); - DateFormatter formatter = new DateFormatter(); - formatter.setIso(DateTimeFormat.ISO.DATE_TIME); - registrar.setFormatter(formatter); - registrar.registerFormatters(service); - return service; + throw exception; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverter.java new file mode 100644 index 0000000000..a8c92d4531 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverter.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2018 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.boot.context.properties.bind.convert; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Stream; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Converts a {@link Delimiter delimited} String to a Collection. + * + * @author Phillip Webb + */ +class DelimitedStringToCollectionConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + DelimitedStringToCollectionConverter(ConversionService conversionService) { + Assert.notNull(conversionService, "ConversionService must not be null"); + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Collection.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return targetType.hasAnnotation(Delimiter.class) + && (targetType.getElementTypeDescriptor() == null + || this.conversionService.canConvert(sourceType, + targetType.getElementTypeDescriptor())); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + return convert((String) source, sourceType, targetType); + } + + private Object convert(String source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + Delimiter delimiter = targetType.getAnnotation(Delimiter.class); + Assert.state(delimiter != null, "Missing @DelimitedStringFormat annotation"); + String[] elements = getElements(source, delimiter.value()); + TypeDescriptor elementDescriptor = targetType.getElementTypeDescriptor(); + Collection target = createCollection(targetType, elementDescriptor, + elements.length); + Stream stream = Arrays.stream(elements).map(String::trim); + if (elementDescriptor != null) { + stream = stream.map((element) -> this.conversionService.convert(element, + sourceType, elementDescriptor)); + } + stream.forEach(target::add); + return target; + } + + private Collection createCollection(TypeDescriptor targetType, + TypeDescriptor elementDescriptor, int length) { + return CollectionFactory.createCollection(targetType.getType(), + (elementDescriptor != null ? elementDescriptor.getType() : null), length); + } + + private String[] getElements(String source, String delimiter) { + return StringUtils.delimitedListToStringArray(source, + Delimiter.NONE.equals(delimiter) ? null : delimiter); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/Delimiter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/Delimiter.java new file mode 100644 index 0000000000..be529fad1e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/Delimiter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2018 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.boot.context.properties.bind.convert; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a field or method parameter should be converted to collection using the + * specified delimiter. + * + * @author Phillip Webb + * @since 2.0.0 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, + ElementType.ANNOTATION_TYPE }) +public @interface Delimiter { + + /** + * A delimiter value used to indicate that no delimiter is required and the result + * should be a single element containing the entire string. + */ + String NONE = ""; + + /** + * The delimiter to use or {@code NONE} if the entire contents should be treated as a + * single element. + * @return the delimiter + */ + String value(); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java index 31c42c80b2..11fde425f8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -31,6 +31,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.boot.context.properties.bind.convert.Delimiter; import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; @@ -210,6 +211,17 @@ public class JavaBeanBinderTests { ExampleEnum.BAR_BAZ); } + @Test + public void bindToClassShouldBindToCollectionWithDelimeter() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.collection", "foo-bar|bar-baz"); + this.sources.add(source); + ExampleCollectionBeanWithDelimeter bean = this.binder + .bind("foo", Bindable.of(ExampleCollectionBeanWithDelimeter.class)).get(); + assertThat(bean.getCollection()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + @Test public void bindToClassWhenHasNoSetterShouldBindToMap() { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); @@ -617,6 +629,21 @@ public class JavaBeanBinderTests { } + public static class ExampleCollectionBeanWithDelimeter { + + @Delimiter("|") + private Collection collection; + + public Collection getCollection() { + return this.collection; + } + + public void setCollection(Collection collection) { + this.collection = collection; + } + + } + public static class ExampleNestedBean { private ExampleValueBean valueBean; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java index 1505138dd9..fed61b8d67 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -19,6 +19,7 @@ package org.springframework.boot.context.properties.bind.convert; import java.io.InputStream; import java.net.InetAddress; import java.time.Duration; +import java.util.List; import org.junit.Before; import org.junit.Test; @@ -27,6 +28,7 @@ import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.io.Resource; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -166,6 +168,36 @@ public class BinderConversionServiceTests { assertThat(converted).isEqualTo(Duration.ofMillis(10)); } + @Test + @SuppressWarnings("unchecked") + public void conversionServiceShouldSupportBarDelimitedStrings() { + this.service = new BinderConversionService(null); + List converted = (List) this.service.convert("ONE|ONE|TWO", + TypeDescriptor.valueOf(String.class), TypeDescriptor.nested( + ReflectionUtils.findField(DelimitedValues.class, "bar"), 0)); + assertThat(converted).containsExactly(TestEnum.ONE, TestEnum.ONE, TestEnum.TWO); + } + + @Test + @SuppressWarnings("unchecked") + public void conversionServiceShouldSupportNoneDelimitedStrings() { + this.service = new BinderConversionService(null); + List converted = (List) this.service.convert("a,b,c", + TypeDescriptor.valueOf(String.class), TypeDescriptor.nested( + ReflectionUtils.findField(DelimitedValues.class, "none"), 0)); + assertThat(converted).containsExactly("a,b,c"); + } + + static class DelimitedValues { + + @Delimiter("|") + List bar; + + @Delimiter(Delimiter.NONE) + List none; + + } + enum TestEnum { ONE, TWO diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverterTests.java new file mode 100644 index 0000000000..d7f093665c --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverterTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2012-2018 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.boot.context.properties.bind.convert; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DelimitedStringToCollectionConverter}. + * + * @author Phillip Webb + */ +public class DelimitedStringToCollectionConverterTests { + + private DefaultFormattingConversionService service; + + private DelimitedStringToCollectionConverter converter; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setup() { + this.service = new DefaultFormattingConversionService(null, false); + this.converter = new DelimitedStringToCollectionConverter(this.service); + this.service.addConverter(this.converter); + DefaultFormattingConversionService.addDefaultFormatters(this.service); + } + + @Test + public void createWhenConversionServiceIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ConversionService must not be null"); + new DelimitedStringToCollectionConverter(null); + } + + @Test + public void getConvertiblePairShouldReturnStringCollectionPair() { + Set types = this.converter.getConvertibleTypes(); + assertThat(types).hasSize(1); + ConvertiblePair pair = types.iterator().next(); + assertThat(pair.getSourceType()).isEqualTo(String.class); + assertThat(pair.getTargetType()).isEqualTo(Collection.class); + } + + @Test + public void matchesWhenTargetIsNotAnnotatedShouldReturnFalse() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor + .nested(ReflectionUtils.findField(Values.class, "noAnnotation"), 0); + assertThat(this.converter.matches(sourceType, targetType)).isFalse(); + } + + @Test + public void matchesWhenHasAnnotationAndNoElementTypeShouldReturnTrue() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor + .nested(ReflectionUtils.findField(Values.class, "noElementType"), 0); + assertThat(this.converter.matches(sourceType, targetType)).isTrue(); + } + + @Test + public void matchesWhenHasAnnotationAndConvertibleElementTypeShouldReturnTrue() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor.nested( + ReflectionUtils.findField(Values.class, "convertibleElementType"), 0); + assertThat(this.converter.matches(sourceType, targetType)).isTrue(); + } + + @Test + public void matchesWhenHasAnnotationAndNonConvertibleElementTypeShouldReturnFalse() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor.nested( + ReflectionUtils.findField(Values.class, "nonConvertibleElementType"), 0); + assertThat(this.converter.matches(sourceType, targetType)).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + public void convertWhenHasNoElementTypeShouldReturnTrimmedString() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor + .nested(ReflectionUtils.findField(Values.class, "noElementType"), 0); + List converted = (List) this.converter.convert(" a | b| c ", + sourceType, targetType); + assertThat(converted).containsExactly("a", "b", "c"); + } + + @Test + @SuppressWarnings("unchecked") + public void convertWhenHasConvertibleElementTypeShouldReturnConvertedType() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor.nested( + ReflectionUtils.findField(Values.class, "convertibleElementType"), 0); + List converted = (List) this.converter.convert(" 1 | 2| 3 ", + sourceType, targetType); + assertThat(converted).containsExactly(1, 2, 3); + } + + @Test + @SuppressWarnings("unchecked") + public void convertWhenHasDelimiterOfNoneShouldReturnTrimmedStringElement() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor + .nested(ReflectionUtils.findField(Values.class, "delimiterNone"), 0); + List converted = (List) this.converter.convert("a,b,c", + sourceType, targetType); + assertThat(converted).containsExactly("a,b,c"); + } + + @Test + public void convertWhenHasCollectionObjectTypeShouldUseCollectionObjectType() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor + .nested(ReflectionUtils.findField(Values.class, "specificType"), 0); + Object converted = this.converter.convert("a*b", sourceType, targetType); + assertThat(converted).isInstanceOf(MyCustomList.class); + } + + static class Values { + + List noAnnotation; + + @SuppressWarnings("rawtypes") + @Delimiter("|") + List noElementType; + + @Delimiter("|") + List convertibleElementType; + + @Delimiter("|") + List nonConvertibleElementType; + + @Delimiter(Delimiter.NONE) + List delimiterNone; + + @Delimiter("*") + MyCustomList specificType; + + } + + static class NonConvertible { + + } + + static class MyCustomList extends LinkedList { + + } + +}