Add support for @Delimiter binding
Add a new `@Delimiter` annotation that can be used to change how a String value is bound to a Collection. Fixes gh-11883
This commit is contained in:
parent
f24c92c658
commit
d6ae4e48d8
|
@ -41,7 +41,7 @@ class ArrayBinder extends IndexedElementsBinder<Object> {
|
||||||
IndexedCollectionSupplier result = new IndexedCollectionSupplier(ArrayList::new);
|
IndexedCollectionSupplier result = new IndexedCollectionSupplier(ArrayList::new);
|
||||||
ResolvableType aggregateType = target.getType();
|
ResolvableType aggregateType = target.getType();
|
||||||
ResolvableType elementType = target.getType().getComponentType();
|
ResolvableType elementType = target.getType().getComponentType();
|
||||||
bindIndexed(name, elementBinder, aggregateType, elementType, result);
|
bindIndexed(name, target, elementBinder, aggregateType, elementType, result);
|
||||||
if (result.wasSupplied()) {
|
if (result.wasSupplied()) {
|
||||||
List<Object> list = (List<Object>) result.get();
|
List<Object> list = (List<Object>) result.get();
|
||||||
Object array = Array.newInstance(elementType.resolve(), list.size());
|
Object array = Array.newInstance(elementType.resolve(), list.size());
|
||||||
|
|
|
@ -45,7 +45,7 @@ class CollectionBinder extends IndexedElementsBinder<Collection<Object>> {
|
||||||
ResolvableType elementType = target.getType().asCollection().getGeneric();
|
ResolvableType elementType = target.getType().asCollection().getGeneric();
|
||||||
IndexedCollectionSupplier result = new IndexedCollectionSupplier(
|
IndexedCollectionSupplier result = new IndexedCollectionSupplier(
|
||||||
() -> CollectionFactory.createCollection(collectionType, 0));
|
() -> CollectionFactory.createCollection(collectionType, 0));
|
||||||
bindIndexed(name, elementBinder, aggregateType, elementType, result);
|
bindIndexed(name, target, elementBinder, aggregateType, elementType, result);
|
||||||
if (result.wasSupplied()) {
|
if (result.wasSupplied()) {
|
||||||
return result.get();
|
return result.get();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.springframework.boot.context.properties.bind;
|
package org.springframework.boot.context.properties.bind;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
@ -56,16 +57,18 @@ abstract class IndexedElementsBinder<T> extends AggregateBinder<T> {
|
||||||
/**
|
/**
|
||||||
* Bind indexed elements to the supplied collection.
|
* Bind indexed elements to the supplied collection.
|
||||||
* @param name The name of the property to bind
|
* @param name The name of the property to bind
|
||||||
|
* @param target the target bindable
|
||||||
* @param elementBinder the binder to use for elements
|
* @param elementBinder the binder to use for elements
|
||||||
* @param aggregateType the aggregate type, may be a collection or an array
|
* @param aggregateType the aggregate type, may be a collection or an array
|
||||||
* @param elementType the element type
|
* @param elementType the element type
|
||||||
* @param result the destination for results
|
* @param result the destination for results
|
||||||
*/
|
*/
|
||||||
protected final void bindIndexed(ConfigurationPropertyName name,
|
protected final void bindIndexed(ConfigurationPropertyName name, Bindable<?> target,
|
||||||
AggregateElementBinder elementBinder, ResolvableType aggregateType,
|
AggregateElementBinder elementBinder, ResolvableType aggregateType,
|
||||||
ResolvableType elementType, IndexedCollectionSupplier result) {
|
ResolvableType elementType, IndexedCollectionSupplier result) {
|
||||||
for (ConfigurationPropertySource source : getContext().getSources()) {
|
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) {
|
if (result.wasSupplied() && result.get() != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -73,12 +76,13 @@ abstract class IndexedElementsBinder<T> extends AggregateBinder<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void bindIndexed(ConfigurationPropertySource source,
|
private void bindIndexed(ConfigurationPropertySource source,
|
||||||
ConfigurationPropertyName root, AggregateElementBinder elementBinder,
|
ConfigurationPropertyName root, Bindable<?> target,
|
||||||
IndexedCollectionSupplier collection, ResolvableType aggregateType,
|
AggregateElementBinder elementBinder, IndexedCollectionSupplier collection,
|
||||||
ResolvableType elementType) {
|
ResolvableType aggregateType, ResolvableType elementType) {
|
||||||
ConfigurationProperty property = source.getConfigurationProperty(root);
|
ConfigurationProperty property = source.getConfigurationProperty(root);
|
||||||
if (property != null) {
|
if (property != null) {
|
||||||
Object aggregate = convert(property.getValue(), aggregateType);
|
Object aggregate = convert(property.getValue(), aggregateType,
|
||||||
|
target.getAnnotations());
|
||||||
ResolvableType collectionType = forClassWithGenerics(
|
ResolvableType collectionType = forClassWithGenerics(
|
||||||
collection.get().getClass(), elementType);
|
collection.get().getClass(), elementType);
|
||||||
Collection<Object> elements = convert(aggregate, collectionType);
|
Collection<Object> elements = convert(aggregate, collectionType);
|
||||||
|
@ -134,10 +138,11 @@ abstract class IndexedElementsBinder<T> extends AggregateBinder<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <C> C convert(Object value, ResolvableType type) {
|
private <C> C convert(Object value, ResolvableType type, Annotation... annotations) {
|
||||||
value = getContext().getPlaceholdersResolver().resolvePlaceholders(value);
|
value = getContext().getPlaceholdersResolver().resolvePlaceholders(value);
|
||||||
BinderConversionService conversionService = getContext().getConversionService();
|
BinderConversionService conversionService = getContext().getConversionService();
|
||||||
return ResolvableTypeDescriptor.forType(type).convert(conversionService, value);
|
return ResolvableTypeDescriptor.forType(type, annotations)
|
||||||
|
.convert(conversionService, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Work around for SPR-16456
|
// Work around for SPR-16456
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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;
|
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 java.util.function.Function;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.bind.Binder;
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
import org.springframework.core.convert.ConversionException;
|
import org.springframework.core.convert.ConversionException;
|
||||||
import org.springframework.core.convert.ConversionService;
|
import org.springframework.core.convert.ConversionService;
|
||||||
import org.springframework.core.convert.ConverterNotFoundException;
|
|
||||||
import org.springframework.core.convert.TypeDescriptor;
|
import org.springframework.core.convert.TypeDescriptor;
|
||||||
import org.springframework.core.convert.support.DefaultConversionService;
|
import org.springframework.core.convert.support.DefaultConversionService;
|
||||||
|
import org.springframework.core.convert.support.GenericConversionService;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.format.datetime.DateFormatter;
|
import org.springframework.format.datetime.DateFormatter;
|
||||||
import org.springframework.format.datetime.DateFormatterRegistrar;
|
import org.springframework.format.datetime.DateFormatterRegistrar;
|
||||||
|
@ -38,69 +41,30 @@ import org.springframework.format.support.DefaultFormattingConversionService;
|
||||||
*/
|
*/
|
||||||
public class BinderConversionService implements ConversionService {
|
public class BinderConversionService implements ConversionService {
|
||||||
|
|
||||||
private static final ConversionService additionalConversionService = createAdditionalConversionService();
|
|
||||||
|
|
||||||
private static final ConversionService defaultConversionService = new DefaultFormattingConversionService();
|
private static final ConversionService defaultConversionService = new DefaultFormattingConversionService();
|
||||||
|
|
||||||
private final ConversionService conversionService;
|
private final List<ConversionService> conversionServices;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link BinderConversionService} instance.
|
* Create a new {@link BinderConversionService} instance.
|
||||||
* @param conversionService and option root conversion service
|
* @param conversionService and option root conversion service
|
||||||
*/
|
*/
|
||||||
public BinderConversionService(ConversionService conversionService) {
|
public BinderConversionService(ConversionService conversionService) {
|
||||||
this.conversionService = (conversionService != null ? conversionService
|
List<ConversionService> conversionServices = new ArrayList<>();
|
||||||
: defaultConversionService);
|
conversionServices.add(createOverrideConversionService());
|
||||||
|
conversionServices.add(
|
||||||
|
conversionService != null ? conversionService : defaultConversionService);
|
||||||
|
conversionServices.add(createAdditionalConversionService());
|
||||||
|
this.conversionServices = Collections.unmodifiableList(conversionServices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private ConversionService createOverrideConversionService() {
|
||||||
public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
|
GenericConversionService service = new GenericConversionService();
|
||||||
return (this.conversionService != null
|
service.addConverter(new DelimitedStringToCollectionConverter(this));
|
||||||
&& this.conversionService.canConvert(sourceType, targetType))
|
return service;
|
||||||
|| additionalConversionService.canConvert(sourceType, targetType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private ConversionService createAdditionalConversionService() {
|
||||||
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
|
||||||
return (this.conversionService != null
|
|
||||||
&& this.conversionService.canConvert(sourceType, targetType))
|
|
||||||
|| additionalConversionService.canConvert(sourceType, targetType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T convert(Object source, Class<T> targetType) {
|
|
||||||
return callConversionService((c) -> c.convert(source, targetType));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object convert(Object source, TypeDescriptor sourceType,
|
|
||||||
TypeDescriptor targetType) {
|
|
||||||
return callConversionService((c) -> c.convert(source, sourceType, targetType));
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> T callConversionService(Function<ConversionService, T> call) {
|
|
||||||
if (this.conversionService == null) {
|
|
||||||
return callAdditionalConversionService(call, null);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return call.apply(this.conversionService);
|
|
||||||
}
|
|
||||||
catch (ConversionException ex) {
|
|
||||||
return callAdditionalConversionService(call, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> T callAdditionalConversionService(Function<ConversionService, T> call,
|
|
||||||
RuntimeException cause) {
|
|
||||||
try {
|
|
||||||
return call.apply(additionalConversionService);
|
|
||||||
}
|
|
||||||
catch (ConverterNotFoundException ex) {
|
|
||||||
throw (cause != null ? cause : ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ConversionService createAdditionalConversionService() {
|
|
||||||
DefaultFormattingConversionService service = new DefaultFormattingConversionService();
|
DefaultFormattingConversionService service = new DefaultFormattingConversionService();
|
||||||
DefaultConversionService.addCollectionConverters(service);
|
DefaultConversionService.addCollectionConverters(service);
|
||||||
service.addConverterFactory(new StringToEnumConverterFactory());
|
service.addConverterFactory(new StringToEnumConverterFactory());
|
||||||
|
@ -117,4 +81,48 @@ public class BinderConversionService implements ConversionService {
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
|
||||||
|
for (ConversionService service : this.conversionServices) {
|
||||||
|
if (service.canConvert(sourceType, targetType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||||
|
for (ConversionService service : this.conversionServices) {
|
||||||
|
if (service.canConvert(sourceType, targetType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T convert(Object source, Class<T> targetType) {
|
||||||
|
return callConversionServices((c) -> c.convert(source, targetType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object convert(Object source, TypeDescriptor sourceType,
|
||||||
|
TypeDescriptor targetType) {
|
||||||
|
return callConversionServices((c) -> c.convert(source, sourceType, targetType));
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T callConversionServices(Function<ConversionService, T> call) {
|
||||||
|
ConversionException exception = null;
|
||||||
|
for (ConversionService service : this.conversionServices) {
|
||||||
|
try {
|
||||||
|
return call.apply(service);
|
||||||
|
}
|
||||||
|
catch (ConversionException ex) {
|
||||||
|
exception = ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<ConvertiblePair> 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<Object> target = createCollection(targetType, elementDescriptor,
|
||||||
|
elements.length);
|
||||||
|
Stream<Object> 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<Object> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
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.bind.handler.IgnoreErrorsBindHandler;
|
||||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
||||||
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
|
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
|
||||||
|
@ -210,6 +211,17 @@ public class JavaBeanBinderTests {
|
||||||
ExampleEnum.BAR_BAZ);
|
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
|
@Test
|
||||||
public void bindToClassWhenHasNoSetterShouldBindToMap() {
|
public void bindToClassWhenHasNoSetterShouldBindToMap() {
|
||||||
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||||
|
@ -617,6 +629,21 @@ public class JavaBeanBinderTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class ExampleCollectionBeanWithDelimeter {
|
||||||
|
|
||||||
|
@Delimiter("|")
|
||||||
|
private Collection<ExampleEnum> collection;
|
||||||
|
|
||||||
|
public Collection<ExampleEnum> getCollection() {
|
||||||
|
return this.collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCollection(Collection<ExampleEnum> collection) {
|
||||||
|
this.collection = collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public static class ExampleNestedBean {
|
public static class ExampleNestedBean {
|
||||||
|
|
||||||
private ExampleValueBean valueBean;
|
private ExampleValueBean valueBean;
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.io.InputStream;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
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.ConversionService;
|
||||||
import org.springframework.core.convert.TypeDescriptor;
|
import org.springframework.core.convert.TypeDescriptor;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.BDDMockito.given;
|
import static org.mockito.BDDMockito.given;
|
||||||
|
@ -166,6 +168,36 @@ public class BinderConversionServiceTests {
|
||||||
assertThat(converted).isEqualTo(Duration.ofMillis(10));
|
assertThat(converted).isEqualTo(Duration.ofMillis(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void conversionServiceShouldSupportBarDelimitedStrings() {
|
||||||
|
this.service = new BinderConversionService(null);
|
||||||
|
List<TestEnum> converted = (List<TestEnum>) 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<String> converted = (List<String>) 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<TestEnum> bar;
|
||||||
|
|
||||||
|
@Delimiter(Delimiter.NONE)
|
||||||
|
List<String> none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
enum TestEnum {
|
enum TestEnum {
|
||||||
|
|
||||||
ONE, TWO
|
ONE, TWO
|
||||||
|
|
|
@ -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<ConvertiblePair> 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<String> converted = (List<String>) 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<Integer> converted = (List<Integer>) 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<String> converted = (List<String>) 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<String> noAnnotation;
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
@Delimiter("|")
|
||||||
|
List noElementType;
|
||||||
|
|
||||||
|
@Delimiter("|")
|
||||||
|
List<Integer> convertibleElementType;
|
||||||
|
|
||||||
|
@Delimiter("|")
|
||||||
|
List<NonConvertible> nonConvertibleElementType;
|
||||||
|
|
||||||
|
@Delimiter(Delimiter.NONE)
|
||||||
|
List<String> delimiterNone;
|
||||||
|
|
||||||
|
@Delimiter("*")
|
||||||
|
MyCustomList<String> specificType;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class NonConvertible {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class MyCustomList<E> extends LinkedList<E> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue