From aca753000ac68580c564f23befaa5bc9eb0aac12 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 17 Apr 2017 11:43:09 -0700 Subject: [PATCH] Add support for configuration properties binding Create a new `Binder` class specifically designed to bind properties from one or more `ConfigurationPropertySources` to an object. The binder provides a replacement for `RelaxedBinder` and attempts to fix the limitations of the previous solution. Closes gh-8868 --- .../properties/bind/AbstractBindHandler.java | 73 ++ .../properties/bind/AggregateBinder.java | 130 +++ .../bind/AggregateElementBinder.java | 53 ++ .../context/properties/bind/ArrayBinder.java | 62 ++ .../context/properties/bind/BeanBinder.java | 37 + .../properties/bind/BeanPropertyBinder.java | 47 + .../properties/bind/BeanPropertyName.java | 58 ++ .../context/properties/bind/BindContext.java | 66 ++ .../properties/bind/BindException.java | 85 ++ .../context/properties/bind/BindHandler.java | 92 ++ .../context/properties/bind/BindResult.java | 167 ++++ .../context/properties/bind/Bindable.java | 234 +++++ .../boot/context/properties/bind/Binder.java | 409 +++++++++ .../properties/bind/CollectionBinder.java | 59 ++ .../bind/IndexedElementsBinder.java | 137 +++ .../properties/bind/JavaBeanBinder.java | 209 +++++ .../context/properties/bind/MapBinder.java | 146 ++++ .../properties/bind/PlaceholdersResolver.java | 44 + .../PropertySourcesPlaceholdersResolver.java | 85 ++ ...boundConfigurationPropertiesException.java | 57 ++ .../bind/convert/BinderConversionService.java | 147 ++++ .../convert/InetAddressToStringConverter.java | 37 + .../bind/convert/PropertyEditorConverter.java | 83 ++ .../convert/ResolvableTypeDescriptor.java | 57 ++ .../convert/StringToCharArrayConverter.java | 33 + .../convert/StringToEnumConverterFactory.java | 88 ++ .../convert/StringToInetAddressConverter.java | 43 + .../properties/bind/convert/package-info.java | 20 + .../bind/handler/IgnoreErrorsBindHandler.java | 48 ++ .../IgnoreNestedPropertiesBindHandler.java | 51 ++ .../handler/NoUnboundElementsBindHandler.java | 91 ++ .../properties/bind/handler/package-info.java | 21 + .../context/properties/bind/package-info.java | 20 + .../validation/BindValidationException.java | 47 + .../validation/OriginTrackedFieldError.java | 61 ++ .../validation/ValidationBindHandler.java | 132 +++ .../bind/validation/ValidationErrors.java | 137 +++ .../bind/validation/package-info.java | 20 + .../properties/bind/ArrayBinderTests.java | 274 ++++++ .../bind/BeanPropertyNameTests.java | 41 + .../properties/bind/BindResultTests.java | 234 +++++ .../properties/bind/BindableTests.java | 210 +++++ .../context/properties/bind/BinderTests.java | 223 +++++ .../bind/CollectionBinderTests.java | 264 ++++++ .../properties/bind/JavaBeanBinderTests.java | 810 ++++++++++++++++++ .../properties/bind/MapBinderTests.java | 355 ++++++++ ...pertySourcesPlaceholdersResolverTests.java | 106 +++ .../convert/AbstractInetAddressTests.java | 40 + .../convert/BinderConversionServiceTests.java | 162 ++++ .../InetAddressToStringConverterTests.java | 47 + .../convert/PropertyEditorConverterTests.java | 96 +++ .../ResolvableTypeDescriptorTests.java | 63 ++ .../StringToCharArrayConverterTests.java | 38 + .../StringToEnumConverterFactoryTests.java | 91 ++ .../StringToInetAddressConverterTests.java | 53 ++ .../handler/IgnoreErrorsBindHandlerTests.java | 85 ++ ...gnoreNestedPropertiesBindHandlerTests.java | 106 +++ .../NoUnboundElementsBindHandlerTests.java | 120 +++ .../test/PackagePrivateBeanBindingTests.java | 81 ++ .../BindValidationExceptionTests.java | 51 ++ .../OriginTrackedFieldErrorTests.java | 64 ++ .../ValidationBindHandlerTests.java | 249 ++++++ .../validation/ValidationErrorsTests.java | 124 +++ .../properties/bind/convert/resource.txt | 1 + 64 files changed, 7374 insertions(+) create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateElementBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindException.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolver.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/UnboundConfigurationPropertiesException.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptor.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/package-info.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/package-info.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldError.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/package-info.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolverTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptorTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandlerTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandlerTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/test/PackagePrivateBeanBindingTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/BindValidationExceptionTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldErrorTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationErrorsTests.java create mode 100644 spring-boot/src/test/resources/org/springframework/boot/context/properties/bind/convert/resource.txt diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java new file mode 100644 index 000000000000..e10177d816d2 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2017 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; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link BindHandler} implementations. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public abstract class AbstractBindHandler implements BindHandler { + + private final BindHandler parent; + + /** + * Create a new binding handler instance. + */ + public AbstractBindHandler() { + this(BindHandler.DEFAULT); + } + + /** + * Create a new binding handler instance with a specific parent. + * @param parent the parent handler + */ + public AbstractBindHandler(BindHandler parent) { + Assert.notNull(parent, "Parent must not be null"); + this.parent = parent; + } + + @Override + public boolean onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + return this.parent.onStart(name, target, context); + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + return this.parent.onSuccess(name, target, context, result); + } + + @Override + public Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + return this.parent.onFailure(name, target, context, error); + } + + @Override + public void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + this.parent.onFinish(name, target, context, result); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java new file mode 100644 index 000000000000..64a6842d71f8 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2017 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; + +import java.util.function.Supplier; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.ResolvableType; + +/** + * Internal strategy used by {@link Binder} to bind aggregates (Maps, Lists, Arrays). + * + * @param the type being bound + * @author Phillip Webb + * @author Madhura Bhave + */ +abstract class AggregateBinder { + + private final BindContext context; + + AggregateBinder(BindContext context) { + this.context = context; + } + + /** + * Perform binding for the aggregate. + * @param name the configuration property name to bind + * @param target the target to bind + * @param itemBinder an item binder + * @return the bound aggregate or null + */ + @SuppressWarnings("unchecked") + public final Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder itemBinder) { + Supplier value = target.getValue(); + Class type = (value == null ? target.getType().resolve() + : ResolvableType.forClass(AggregateBinder.class, getClass()) + .resolveGeneric()); + Object result = bind(name, target, itemBinder, type); + if (result == null || value == null || value.get() == null) { + return result; + } + return merge((T) value.get(), (T) result); + } + + /** + * Perform the actual aggregate binding. + * @param name the configuration property name to bind + * @param target the target to bind + * @param elementBinder an element binder + * @param type the aggregate actual type to use + * @return the bound result + */ + protected abstract Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, Class type); + + /** + * Merge any additional elements into the existing aggregate. + * @param existing the existing value + * @param additional the additional elements to merge + * @return the merged result + */ + protected abstract T merge(T existing, T additional); + + /** + * Return the context being used by this binder. + * @return the context + */ + protected final BindContext getContext() { + return this.context; + } + + /** + * Roll up the given name to the first element below the root. For example a name of + * {@code foo.bar.baz} rolled up to the root {@code foo} would be {@code foo.bar}. + * @param name the name to roll up + * @param root the root name + * @return the rolled up name or {@code null} + */ + protected final ConfigurationPropertyName rollUp(ConfigurationPropertyName name, + ConfigurationPropertyName root) { + while (name != null && (name.getParent() != null) + && (!root.equals(name.getParent()))) { + name = name.getParent(); + } + return name; + } + + /** + * Internal class used to supply the aggregate and cache the value. + * @param The aggregate type + */ + protected static class AggregateSupplier { + + private final Supplier supplier; + + private T supplied; + + public AggregateSupplier(Supplier supplier) { + this.supplier = supplier; + } + + public T get() { + if (this.supplied == null) { + this.supplied = this.supplier.get(); + } + return this.supplied; + } + + public boolean wasSupplied() { + return this.supplied != null; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateElementBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateElementBinder.java new file mode 100644 index 000000000000..a8edc73f7144 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateElementBinder.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2017 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; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; + +/** + * Binder that can be used by {@link AggregateBinder} implementations to recursively bind + * elements. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +@FunctionalInterface +interface AggregateElementBinder { + + /** + * Bind the given name to a target bindable. + * @param name the name to bind + * @param target the target bindable + * @return a bound object or {@code null} + */ + default Object bind(ConfigurationPropertyName name, Bindable target) { + return bind(name, target, null); + } + + /** + * Bind the given name to a target bindable using optionally limited to a single + * source. + * @param name the name to bind + * @param target the target bindable + * @param source the source of the elements or {@code null} to use all sources + * @return a bound object or {@code null} + */ + Object bind(ConfigurationPropertyName name, Bindable target, + ConfigurationPropertySource source); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java new file mode 100644 index 000000000000..eb981e2dbe48 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2017 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; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.ResolvableType; + +/** + * {@link AggregateBinder} for arrays. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ArrayBinder extends IndexedElementsBinder { + + ArrayBinder(BindContext context) { + super(context); + } + + @Override + protected Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, Class type) { + IndexedCollectionSupplier collection = new IndexedCollectionSupplier( + ArrayList::new); + ResolvableType elementType = target.getType().getComponentType(); + bindIndexed(name, target, elementBinder, collection, target.getType(), + elementType); + if (collection.wasSupplied()) { + List list = (List) collection.get(); + Object array = Array.newInstance(elementType.resolve(), list.size()); + for (int i = 0; i < list.size(); i++) { + Array.set(array, i, list.get(i)); + } + return array; + } + return null; + } + + @Override + protected Object merge(Object existing, Object additional) { + return additional; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java new file mode 100644 index 000000000000..20b60d4d9475 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2017 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; + +/** + * Internal strategy used by {@link Binder} to bind beans. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +interface BeanBinder { + + /** + * Return a bound bean instance or {@code null} if the {@link BeanBinder} does not + * support the specified {@link Bindable}. + * @param bindable the binable to bind + * @param propertyBinder property binder + * @param The source type + * @return a bound instance or {@code null} + */ + T bind(Bindable bindable, BeanPropertyBinder propertyBinder); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java new file mode 100644 index 000000000000..313d030edd2d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2017 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; + +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; + +/** + * Binder that can be used by {@link BeanBinder} implementations to recursively bind bean + * properties. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +interface BeanPropertyBinder { + + /** + * Return {@code true} if this binder has known bindable elements. If names from + * underlying {@link ConfigurationPropertySource} cannot be iterated this method can + * return {@code false}, even though binding may ultimately succeed. + * @return true if there are known bindable properties + */ + boolean hasKnownBindableProperties(); + + /** + * Bind the given property. + * @param propertyName the property name (in lowercase dashed form, e.g. + * {@code first-name}) + * @param target the target bindable + * @return the bound value or {@code null} + */ + Object bindProperty(String propertyName, Bindable target); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java new file mode 100644 index 000000000000..e76a904bbb6d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2017 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; + +import org.springframework.core.convert.Property; + +/** + * Internal utility to help when dealing with Java Bean property names. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +abstract class BeanPropertyName { + + private BeanPropertyName() { + } + + /** + * Return the name of the specified {@link Property} in dashed form. + * @param property the source property + * @return the dashed from + */ + public static String toDashedForm(Property property) { + return toDashedForm(property.getName()); + } + + /** + * Return the specified Java Bean property name in dashed form. + * @param name the source name + * @return the dashed from + */ + public static String toDashedForm(String name) { + StringBuilder result = new StringBuilder(); + for (char c : name.replace("_", "-").toCharArray()) { + if (Character.isUpperCase(c) && result.length() > 0 + && result.charAt(result.length() - 1) != '-') { + result.append("-"); + } + result.append(Character.toLowerCase(c)); + } + return result.toString(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java new file mode 100644 index 000000000000..b4f0001c3cac --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2017 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; + +import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.core.convert.ConversionService; + +/** + * Context information for use by {@link BindHandler BindHandlers}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public interface BindContext { + + /** + * Return the current depth of the binding. Root binding starts with a depth of + * {@code 0}. Each subsequent property binding increases the depth by {@code 1}. + * @return the depth of the current binding + */ + int getDepth(); + + /** + * Return the {@link ConfigurationPropertySource sources} being used by the + * {@link Binder}. + * @return the sources + */ + Iterable getSources(); + + /** + * Return the {@link ConfigurationProperty} actually being bound or {@code null} if + * the property has not yet been determined. + * @return the configuration property (may be {@code null}). + */ + ConfigurationProperty getConfigurationProperty(); + + /** + * Return the {@link PlaceholdersResolver} being used by the binder. + * @return the {@link PlaceholdersResolver} (never {@code null}) + */ + PlaceholdersResolver getPlaceholdersResolver(); + + /** + * Return the {@link ConversionService} used by the binder. + * @return the conversion service + */ + BinderConversionService getConversionService(); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindException.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindException.java new file mode 100644 index 000000000000..ddf5d4ba62a0 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindException.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2017 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; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; + +/** + * Exception thrown when binding fails. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class BindException extends RuntimeException implements OriginProvider { + + private final Bindable target; + + private final ConfigurationProperty property; + + private final ConfigurationPropertyName name; + + BindException(ConfigurationPropertyName name, Bindable target, + ConfigurationProperty property, Throwable cause) { + super(buildMessage(name, target), cause); + this.name = name; + this.target = target; + this.property = property; + } + + /** + * Return the name of the configuration property being bound. + * @return the configuration property name + */ + public ConfigurationPropertyName getName() { + return this.name; + } + + /** + * Return the target being bound. + * @return the bind target + */ + public Bindable getTarget() { + return this.target; + } + + /** + * Return the configuration property name of the item that was being bound. + * @return the configuration property name + */ + public ConfigurationProperty getProperty() { + return this.property; + } + + @Override + public Origin getOrigin() { + return Origin.from(this.name); + } + + private static String buildMessage(ConfigurationPropertyName name, + Bindable target) { + StringBuilder message = new StringBuilder(); + message.append("Failed to bind properties"); + message.append(name == null ? "" : " under '" + name + "'"); + message.append(" to " + target.getType()); + return message.toString(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java new file mode 100644 index 000000000000..b564b8eb3737 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2017 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; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; + +/** + * Callback interface that can be used to handle additional logic during element + * {@link Binder binding}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public interface BindHandler { + + /** + * Default no-op bind hander. + */ + BindHandler DEFAULT = new BindHandler() { + + }; + + /** + * Called when binding of an element starts but before any result has been determined. + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @return {@code true} if binding should proceed + */ + default boolean onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + return true; + } + + /** + * Called when binding of an element ends with a successful result. Implementations + * may change the ultimately returned result or perform addition validation. + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @param result the bound result (never {@code null}) + * @return the actual result that should be used (may be {@code null}) + */ + default Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + return result; + } + + /** + * Called when binding fails for any reason (including failures from + * {@link #onSuccess} calls). Implementations may chose to swallow exceptions and + * return an alternative result. + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @param error the cause of the error (if the exception stands it may be re-thrown) + * @return the actual result that should be used (may be {@code null}). + * @throws Exception if the binding isn't valid + */ + default Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + throw error; + } + + /** + * Called when binding finishes, regardless of whether the property was bound or not. + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @param result the bound result (may be {@code null}) + * @throws Exception if the binding isn't valid + */ + default void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java new file mode 100644 index 000000000000..72754f7bc3f4 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java @@ -0,0 +1,167 @@ +/* + * Copyright 2012-2017 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; + +import java.util.NoSuchElementException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A container object to return result of a {@link Binder} bind operation. May contain + * either a successfully bound object or an empty result. + * + * @param The result type + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public final class BindResult { + + private static final BindResult UNBOUND = new BindResult<>(null); + + private final T value; + + private BindResult(T value) { + this.value = value; + } + + /** + * Return the object that was bound or throw a {@link NoSuchElementException} if no + * value was bound. + * @return the the bound value (never {@code null}) + * @throws NoSuchElementException if no value was bound + * @see #isBound() + */ + public T get() throws NoSuchElementException { + if (this.value == null) { + throw new NoSuchElementException("No value bound"); + } + return this.value; + } + + /** + * Returns {@code true} if a result was bound. + * @return if a result was bound + */ + public boolean isBound() { + return (this.value != null); + } + + /** + * Invoke the specified consumer with the bound value, or do nothing if no value has + * been bound. + * @param consumer block to execute if a value has been bound + */ + public void ifBound(Consumer consumer) { + Assert.notNull(consumer, "Consumer must not be null"); + if (this.value != null) { + consumer.accept(this.value); + } + } + + /** + * Apply the provided mapping function to the bound value, or return an updated + * unbound result if no value has been bound. + * @param The type of the result of the mapping function + * @param mapper a mapping function to apply to the bound value. The mapper will not + * be invoked if no value has been bound. + * @return an {@code BindResult} describing the result of applying a mapping function + * to the value of this {@code BindResult}. + */ + public BindResult map(Function mapper) { + Assert.notNull(mapper, "Mapper must not be null"); + return of(this.value == null ? null : mapper.apply(this.value)); + } + + /** + * Return the object that was bound, or {@code other} if no value has been bound. + * @param other the value to be returned if there is no bound value (may be + * {@code null}) + * @return the value, if bound, otherwise {@code other} + */ + public T orElse(T other) { + return (this.value != null ? this.value : other); + } + + /** + * Return the object that was bound, or the result of invoking {@code other} if no + * value has been bound. + * @param other a {@link Supplier} of the value to be returned if there is no bound + * value + * @return the value, if bound, otherwise the supplied {@code other} + */ + public T orElseGet(Supplier other) { + return (this.value != null ? this.value : other.get()); + } + + /** + * Return the object that was bound, or a new instance of the specified class if no + * value has been bound. + * @param type the type to create if no value was bound + * @return the value, if bound, otherwise a new instance of {@code type} + */ + public T orElseCreate(Class type) { + Assert.notNull(type, "Type must not be null"); + return (this.value != null ? this.value : BeanUtils.instantiateClass(type)); + } + + /** + * Return the object that was bound, or throw an exception to be created by the + * provided supplier if no value has been bound. + * @param Type of the exception to be thrown + * @param exceptionSupplier The supplier which will return the exception to be thrown + * @return the present value + * @throws X if there is no value present + */ + public T orElseThrow(Supplier exceptionSupplier) + throws X { + if (this.value == null) { + throw exceptionSupplier.get(); + } + return this.value; + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return ObjectUtils.nullSafeEquals(this.value, ((BindResult) obj).value); + } + + @SuppressWarnings("unchecked") + static BindResult of(T value) { + if (value == null) { + return (BindResult) UNBOUND; + } + return new BindResult(value); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java new file mode 100644 index 000000000000..47a02767ab7d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java @@ -0,0 +1,234 @@ +/* + * Copyright 2012-2017 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; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Source that can be bound by a {@link Binder}. + * + * @param The source type + * @author Philip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see Bindable#of(Class) + * @see Bindable#of(ResolvableType) + */ +public final class Bindable { + + private static final Annotation[] NO_ANNOTATIONS = {}; + + private final ResolvableType type; + + private final ResolvableType boxedType; + + private final Supplier value; + + private final Annotation[] annotations; + + private Bindable(ResolvableType type, ResolvableType boxedType, Supplier value, + Annotation[] annotations) { + this.type = type; + this.boxedType = boxedType; + this.value = value; + this.annotations = annotations; + } + + /** + * Return the type of the item to bind. + * @return the type being bound + */ + public ResolvableType getType() { + return this.type; + } + + public ResolvableType getBoxedType() { + return this.boxedType; + } + + /** + * Return a supplier that provides the object value or {@code null}. + * @return the value or {@code null} + */ + public Supplier getValue() { + return this.value; + } + + /** + * Return any associated annotations that could affect binding. + * @return the associated annotations + */ + public Annotation[] getAnnotations() { + return this.annotations; + } + + @Override + public String toString() { + ToStringCreator creator = new ToStringCreator(this); + creator.append("type", this.type); + creator.append("value", (this.value == null ? "none" : "provided")); + creator.append("annotations", this.annotations); + return creator.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ObjectUtils.nullSafeHashCode(this.type); + result = prime * result + ObjectUtils.nullSafeHashCode(this.annotations); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Bindable other = (Bindable) obj; + boolean result = true; + result = result && nullSafeEquals(this.type.resolve(), other.type.resolve()); + result = result && nullSafeEquals(this.annotations, other.annotations); + return result; + } + + private boolean nullSafeEquals(Object o1, Object o2) { + return ObjectUtils.nullSafeEquals(o1, o2); + } + + /** + * Create an updated {@link Bindable} instance with the specified annotations. + * @param annotations the annotations + * @return an updated {@link Bindable} + */ + public Bindable withAnnotations(Annotation... annotations) { + return new Bindable(this.type, this.boxedType, this.value, annotations); + } + + public Bindable withExistingValue(T existingValue) { + Assert.isTrue( + existingValue == null || this.type.isArray() + || this.boxedType.resolve().isInstance(existingValue), + "ExistingValue must be an instance of " + this.type); + Supplier value = (existingValue == null ? null : () -> existingValue); + return new Bindable<>(this.type, this.boxedType, value, NO_ANNOTATIONS); + } + + public Bindable withSuppliedValue(Supplier suppliedValue) { + return new Bindable<>(this.type, this.boxedType, suppliedValue, NO_ANNOTATIONS); + } + + /** + * Create a new {@link Bindable} of the type of the specified instance with an + * existing value equal to the instance. + * @param The source type + * @param instance the instance (must not be {@code null}) + * @return a {@link Bindable} instance + * @see #of(ResolvableType) + * @see #withExistingValue(Object) + */ + @SuppressWarnings("unchecked") + public static Bindable ofInstance(T instance) { + Assert.notNull(instance, "Instance must not be null"); + Class type = (Class) instance.getClass(); + return of(type).withExistingValue(instance); + } + + /** + * Create a new {@link Bindable} of the specified type. + * @param The source type + * @param type the type (must not be {@code null}) + * @return a {@link Bindable} instance + * @see #of(ResolvableType) + */ + public static Bindable of(Class type) { + Assert.notNull(type, "Type must not be null"); + return of(ResolvableType.forClass(type)); + } + + /** + * Create a new {@link Bindable} {@link List} of the specified element type. + * @param the element type + * @param elementType the list element type + * @return a {@link Bindable} instance + */ + public static Bindable> listOf(Class elementType) { + return of(ResolvableType.forClassWithGenerics(List.class, elementType)); + } + + /** + * Create a new {@link Bindable} {@link Set} of the specified element type. + * @param the element type + * @param elementType the set element type + * @return a {@link Bindable} instance + */ + public static Bindable> setOf(Class elementType) { + return of(ResolvableType.forClassWithGenerics(Set.class, elementType)); + } + + /** + * Create a new {@link Bindable} {@link Map} of the specified kay and value type. + * @param the key type + * @param the value type + * @param keyType the map key type + * @param valueType the map value type + * @return a {@link Bindable} instance + */ + public static Bindable> mapOf(Class keyType, Class valueType) { + return of(ResolvableType.forClassWithGenerics(Map.class, keyType, valueType)); + } + + /** + * Create a new {@link Bindable} of the specified type. + * @param The source type + * @param type the type (must not be {@code null}) + * @return a {@link Bindable} instance + * @see #of(Class) + */ + public static Bindable of(ResolvableType type) { + Assert.notNull(type, "Type must not be null"); + ResolvableType boxedType = box(type); + return new Bindable<>(type, boxedType, null, NO_ANNOTATIONS); + } + + private static ResolvableType box(ResolvableType type) { + Class resolved = type.resolve(); + if (resolved != null && resolved.isPrimitive()) { + Object array = Array.newInstance(resolved, 1); + Class wrapperType = Array.get(array, 0).getClass(); + return ResolvableType.forClass(wrapperType); + } + if (resolved.isArray()) { + return ResolvableType.forArrayComponent(box(type.getComponentType())); + } + return type; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java new file mode 100644 index 000000000000..c50f35ff75f7 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -0,0 +1,409 @@ +/* + * Copyright 2012-2017 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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * A container object which Binds objects from one or more + * {@link ConfigurationPropertySource ConfigurationPropertySources}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class Binder { + + private static final Set> NON_BEAN_CLASSES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(Object.class, Class.class))); + + private static final List BEAN_BINDERS; + + static { + List beanBinders = new ArrayList<>(); + beanBinders.add(new JavaBeanBinder()); + BEAN_BINDERS = Collections.unmodifiableList(beanBinders); + } + + private final Iterable sources; + + private final PlaceholdersResolver placeholdersResolver; + + private final BinderConversionService conversionService; + + /** + * Create a new {@link Binder} instance for the specified sources. A + * {@link DefaultFormattingConversionService} will be used for all conversion. + * @param sources the sources used for binding + */ + public Binder(ConfigurationPropertySource... sources) { + this(Arrays.asList(sources), null, null); + } + + /** + * Create a new {@link Binder} instance for the specified sources. A + * {@link DefaultFormattingConversionService} will be used for all conversion. + * @param sources the sources used for binding + */ + public Binder(Iterable sources) { + this(sources, null, null); + } + + /** + * Create a new {@link Binder} instance for the specified sources. + * @param sources the sources used for binding + * @param placeholdersResolver strategy to resolve any property place-holders + */ + public Binder(Iterable sources, + PlaceholdersResolver placeholdersResolver) { + this(sources, placeholdersResolver, null); + } + + /** + * Create a new {@link Binder} instance for the specified sources. + * @param sources the sources used for binding + * @param placeholdersResolver strategy to resolve any property place-holders + * @param conversionService the conversion service to convert values + */ + public Binder(Iterable sources, + PlaceholdersResolver placeholdersResolver, + ConversionService conversionService) { + Assert.notNull(sources, "Sources must not be null"); + this.sources = sources; + this.placeholdersResolver = (placeholdersResolver != null ? placeholdersResolver + : PlaceholdersResolver.NONE); + this.conversionService = new BinderConversionService(conversionService); + } + + /** + * Bind the specified target {@link Class} using this binders + * {@link ConfigurationPropertySource property sources}. + * @param name the configuration property name to bind + * @param target the target class + * @param the bound type + * @return the binding result (never {@code null}) + * @see #bind(ConfigurationPropertyName, Bindable, BindHandler) + */ + public BindResult bind(String name, Class target) { + return bind(name, Bindable.of(target)); + } + + /** + * Bind the specified target {@link Bindable} using this binders + * {@link ConfigurationPropertySource property sources}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param the bound type + * @return the binding result (never {@code null}) + * @see #bind(ConfigurationPropertyName, Bindable, BindHandler) + */ + public BindResult bind(String name, Bindable target) { + return bind(ConfigurationPropertyName.of(name), target, null); + } + + /** + * Bind the specified target {@link Bindable} using this binders + * {@link ConfigurationPropertySource property sources}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param the bound type + * @return the binding result (never {@code null}) + * @see #bind(ConfigurationPropertyName, Bindable, BindHandler) + */ + public BindResult bind(ConfigurationPropertyName name, Bindable target) { + return bind(name, target, null); + } + + /** + * Bind the specified target {@link Bindable} using this binders + * {@link ConfigurationPropertySource property sources}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param handler the bind handler (may be {@code null}) + * @param the bound type + * @return the binding result (never {@code null}) + */ + public BindResult bind(String name, Bindable target, BindHandler handler) { + return bind(ConfigurationPropertyName.of(name), target, handler); + } + + /** + * Bind the specified target {@link Bindable} using this binders + * {@link ConfigurationPropertySource property sources}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param handler the bind handler (may be {@code null}) + * @param the bound type + * @return the binding result (never {@code null}) + */ + public BindResult bind(ConfigurationPropertyName name, Bindable target, + BindHandler handler) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(target, "Target must not be null"); + handler = (handler != null ? handler : BindHandler.DEFAULT); + Context context = new Context(); + T bound = bind(name, target, handler, context); + return BindResult.of(bound); + } + + protected final T bind(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context) { + try { + if (!handler.onStart(name, target, context)) { + return null; + } + Object bound = bindObject(name, target, handler, context); + return handleBindResult(name, target, handler, context, bound); + } + catch (Exception ex) { + return handleBindError(name, target, handler, context, ex); + } + } + + private T handleBindResult(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, Object result) throws Exception { + result = convert(result, target); + if (result != null) { + result = handler.onSuccess(name, target, context, result); + result = convert(result, target); + } + handler.onFinish(name, target, context, result); + return convert(result, target); + } + + private T handleBindError(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, Exception error) { + try { + Object result = handler.onFailure(name, target, context, error); + return convert(result, target); + } + catch (Exception ex) { + if (ex instanceof BindException) { + throw (BindException) ex; + } + throw new BindException(name, target, context.getConfigurationProperty(), ex); + } + } + + private T convert(Object value, Bindable target) { + if (value == null) { + return null; + } + return this.conversionService.convert(value, target); + } + + private Object bindObject(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context) throws Exception { + AggregateBinder aggregateBinder = getAggregateBinder(target, context); + if (aggregateBinder != null) { + return bindAggregate(name, target, handler, context, aggregateBinder); + } + ConfigurationProperty property = findProperty(name); + if (property != null) { + return bindProperty(name, target, handler, context, property); + } + return bindBean(name, target, handler, context); + } + + private AggregateBinder getAggregateBinder(Bindable target, Context context) { + if (Map.class.isAssignableFrom(target.getType().resolve())) { + return new MapBinder(context); + } + if (Collection.class.isAssignableFrom(target.getType().resolve())) { + return new CollectionBinder(context); + } + if (target.getType().isArray()) { + return new ArrayBinder(context); + } + return null; + } + + private Object bindAggregate(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, AggregateBinder aggregateBinder) { + AggregateElementBinder elementBinder = (itemName, itemTarget, source) -> { + Binder binder = (source == null ? Binder.this : new Binder(source)); + return binder.bind(itemName, itemTarget, handler, context.increaseDepth()); + }; + return aggregateBinder.bind(name, target, elementBinder); + } + + private ConfigurationProperty findProperty(ConfigurationPropertyName name) { + return streamSources().map((source) -> source.getConfigurationProperty(name)) + .filter(Objects::nonNull).findFirst().orElse(null); + } + + private Object bindProperty(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, ConfigurationProperty property) { + context.setConfigurationProperty(property); + Object result = property.getValue(); + result = this.placeholdersResolver.resolvePlaceholders(result); + result = this.conversionService.convert(result, target); + return result; + } + + private Object bindBean(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context) throws Exception { + BeanPropertyBinder propertyBinder = getPropertyBinder(context.increaseDepth(), + name, handler); + boolean noKnownBindableProperties = !propertyBinder.hasKnownBindableProperties(); + if (noKnownBindableProperties && isUnbindableBean(target)) { + return null; + } + Class type = target.getType().resolve(); + if (context.hasBoundBean(type)) { + return null; + } + context.setBean(type); + return BEAN_BINDERS.stream().map((b) -> b.bind(target, propertyBinder)) + .filter(Objects::nonNull).findFirst().orElse(null); + } + + private BeanPropertyBinder getPropertyBinder(Context context, + ConfigurationPropertyName name, BindHandler handler) { + return new BeanPropertyBinder() { + + @Override + public boolean hasKnownBindableProperties() { + return streamSources() + .flatMap((s) -> s.filter(name::isAncestorOf).stream()).findAny() + .isPresent(); + } + + @Override + public Object bindProperty(String propertyName, Bindable target) { + return Binder.this.bind(name.append(propertyName), target, handler, + context); + } + + }; + } + + private boolean isUnbindableBean(Bindable target) { + Class resolved = target.getType().resolve(); + if (NON_BEAN_CLASSES.contains(resolved)) { + return true; + } + String packageName = ClassUtils.getPackageName(resolved); + return packageName.startsWith("java."); + } + + private Stream streamSources() { + return StreamSupport.stream(this.sources.spliterator(), false); + } + + /** + * Create a new {@link Binder} instance from the specified environment. + * @param environment the environment (must be a {@link ConfigurableEnvironment}) + * @return a {@link Binder} instance + */ + public static Binder get(Environment environment) { + Assert.isInstanceOf(ConfigurableEnvironment.class, environment); + return new Binder( + ConfigurationPropertySources.get((ConfigurableEnvironment) environment), + new PropertySourcesPlaceholdersResolver(environment)); + } + + /** + * {@link BindContext} implementation. + */ + final class Context implements BindContext { + + private final Context parent; + + private ConfigurationProperty configurationProperty; + + private Class bean; + + Context() { + this(null, null); + } + + Context(Context parent, Class bean) { + this.parent = parent; + this.bean = bean; + } + + public boolean hasBoundBean(Class bean) { + if (this.bean != null && this.bean.equals(bean)) { + return true; + } + return (this.parent != null ? this.parent.hasBoundBean(bean) : false); + } + + public void setBean(Class bean) { + this.bean = bean; + } + + public Context increaseDepth() { + return new Context(this, null); + } + + @Override + public int getDepth() { + return (this.parent == null ? 0 : this.parent.getDepth() + 1); + } + + @Override + public Iterable getSources() { + return Binder.this.sources; + } + + @Override + public ConfigurationProperty getConfigurationProperty() { + return this.configurationProperty; + } + + public void setConfigurationProperty( + ConfigurationProperty configurationProperty) { + this.configurationProperty = configurationProperty; + } + + @Override + public PlaceholdersResolver getPlaceholdersResolver() { + return Binder.this.placeholdersResolver; + } + + @Override + public BinderConversionService getConversionService() { + return Binder.this.conversionService; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java new file mode 100644 index 000000000000..96aa7885b3c3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2017 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; + +import java.util.Collection; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; + +/** + * {@link AggregateBinder} for collections. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class CollectionBinder extends IndexedElementsBinder> { + + CollectionBinder(BindContext context) { + super(context); + } + + @Override + protected Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, Class type) { + IndexedCollectionSupplier collection = new IndexedCollectionSupplier( + () -> CollectionFactory.createCollection(type, 0)); + ResolvableType elementType = target.getType().asCollection().getGeneric(); + bindIndexed(name, target, elementBinder, collection, target.getType(), + elementType); + if (collection.wasSupplied()) { + return collection.get(); + } + return null; + } + + @Override + protected Collection merge(Collection existing, + Collection additional) { + existing.clear(); + existing.addAll(additional); + return existing; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java new file mode 100644 index 000000000000..fceb609e433d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2017 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; + +import java.util.Collection; +import java.util.List; +import java.util.TreeSet; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.core.ResolvableType; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Base class for {@link AggregateBinder AggregateBinders} that read a sequential run of + * indexed items. + * + * @param the type being bound + * @author Phillip Webb + * @author Madhura Bhave + */ +abstract class IndexedElementsBinder extends AggregateBinder { + + IndexedElementsBinder(BindContext context) { + super(context); + } + + protected final void bindIndexed(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, IndexedCollectionSupplier collection, + ResolvableType aggregateType, ResolvableType elementType) { + for (ConfigurationPropertySource source : getContext().getSources()) { + bindIndexed(source, name, elementBinder, collection, aggregateType, + elementType); + if (collection.wasSupplied() && collection.get() != null) { + return; + } + } + } + + private void bindIndexed(ConfigurationPropertySource source, + ConfigurationPropertyName root, AggregateElementBinder elementBinder, + IndexedCollectionSupplier collection, ResolvableType aggregateType, + ResolvableType elementType) { + ConfigurationProperty property = source.getConfigurationProperty(root); + if (property != null) { + Object aggregate = convert(property.getValue(), aggregateType); + ResolvableType collectionType = ResolvableType + .forClassWithGenerics(collection.get().getClass(), elementType); + Collection elements = convert(aggregate, collectionType); + collection.get().addAll(elements); + } + else { + bindIndexed(source, root, elementBinder, collection, elementType); + } + } + + private void bindIndexed(ConfigurationPropertySource source, + ConfigurationPropertyName root, AggregateElementBinder elementBinder, + IndexedCollectionSupplier collection, ResolvableType elementType) { + MultiValueMap knownIndexedChildren = getKnownIndexedChildren( + source, root); + for (int i = 0; i < Integer.MAX_VALUE; i++) { + ConfigurationPropertyName name = root.append("[" + i + "]"); + Object value = elementBinder.bind(name, Bindable.of(elementType), source); + if (value == null) { + break; + } + knownIndexedChildren.remove(name.getElement().getValue(Form.UNIFORM)); + collection.get().add(value); + } + assertNoUnboundChildren(knownIndexedChildren); + } + + private MultiValueMap getKnownIndexedChildren( + ConfigurationPropertySource source, ConfigurationPropertyName root) { + MultiValueMap children = new LinkedMultiValueMap<>(); + for (ConfigurationPropertyName name : source.filter(root::isAncestorOf)) { + name = rollUp(name, root); + if (name.getElement().isIndexed()) { + String key = name.getElement().getValue(Form.UNIFORM); + ConfigurationProperty value = source.getConfigurationProperty(name); + children.add(key, value); + } + } + return children; + } + + private void assertNoUnboundChildren( + MultiValueMap children) { + if (!children.isEmpty()) { + throw new UnboundConfigurationPropertiesException( + children.values().stream().flatMap(List::stream) + .collect(Collectors.toCollection(TreeSet::new))); + } + } + + @SuppressWarnings("unchecked") + private C convert(Object value, ResolvableType type) { + value = getContext().getPlaceholdersResolver().resolvePlaceholders(value); + BinderConversionService conversionService = getContext().getConversionService(); + return (C) conversionService.convert(value, type); + } + + /** + * {@link AggregateBinder.AggregateSupplier AggregateSupplier} for an index + * collection. + */ + protected static class IndexedCollectionSupplier + extends AggregateSupplier> { + + public IndexedCollectionSupplier(Supplier> supplier) { + super(supplier); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java new file mode 100644 index 000000000000..80ab0a6573cb --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java @@ -0,0 +1,209 @@ +/* + * Copyright 2012-2017 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; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.beans.BeanUtils; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.Property; + +/** + * {@link BeanBinder} for mutable Java Beans. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class JavaBeanBinder implements BeanBinder { + + @Override + public T bind(Bindable bindable, BeanPropertyBinder propertyBinder) { + Bean bean = Bean.get(bindable, propertyBinder.hasKnownBindableProperties()); + if (bean == null) { + return null; + } + boolean bound = bind(bean, propertyBinder); + return (bound ? bean.getInstance() : null); + } + + private boolean bind(Bean bean, BeanPropertyBinder propertyBinder) { + boolean bound = false; + for (Property property : bean.getProperties()) { + Object boundValue = bind(bean, property, propertyBinder); + bound |= boundValue != null; + } + return bound; + } + + private Object bind(Bean bean, Property property, + BeanPropertyBinder propertyBinder) { + ResolvableType type = getResolvableType(property); + Supplier value = bean.getPropertyValue(property); + String propertyName = BeanPropertyName.toDashedForm(property); + Annotation[] annotations = getAnnotations(bean, property); + Object bound = propertyBinder.bindProperty(propertyName, + Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations)); + if (bound == null) { + return null; + } + if (property.getWriteMethod() != null) { + bean.setPropertyValue(property, bound); + } + else if (value == null || !bound.equals(value.get())) { + throw new IllegalStateException( + "No setter found for property: " + property.getName()); + } + return bound; + } + + private ResolvableType getResolvableType(Property property) { + if (property.getWriteMethod() != null) { + return ResolvableType.forMethodParameter(property.getWriteMethod(), 0); + } + else { + return ResolvableType.forMethodReturnType(property.getReadMethod()); + } + } + + private Annotation[] getAnnotations(Bean bean, Property property) { + try { + Field field = bean.getType().getDeclaredField(property.getName()); + return field.getDeclaredAnnotations(); + } + catch (Exception ex) { + return null; + } + } + + /** + * The bean being bound. + */ + private static class Bean { + + private final Class type; + + private final Supplier existingValue; + + private final List properties; + + private T instance; + + Bean(Class type, Supplier existingValue) { + this.type = type; + this.existingValue = existingValue; + this.properties = convertToProperties(type, + BeanUtils.getPropertyDescriptors(type)); + } + + private List convertToProperties(Class type, + PropertyDescriptor[] descriptors) { + Stream properties = Arrays.stream(descriptors) + .map((descriptor) -> convertToProperty(type, descriptor)) + .filter(p -> !isFiltered(p)); + return Collections.unmodifiableList(properties.collect(Collectors.toList())); + } + + private Property convertToProperty(Class type, PropertyDescriptor descriptor) { + return new Property(type, descriptor.getReadMethod(), + descriptor.getWriteMethod(), descriptor.getName()); + } + + private boolean isFiltered(Property property) { + return "class".equals(property.getName()); + } + + public List getProperties() { + return this.properties; + } + + public Class getType() { + return this.type; + } + + public Supplier getPropertyValue(Property property) { + Method readMethod = property.getReadMethod(); + if (readMethod == null) { + return null; + } + return () -> { + try { + readMethod.setAccessible(true); + return readMethod.invoke(getInstance()); + } + catch (Exception ex) { + throw new IllegalStateException( + "Unable to get value for property " + property.getName(), ex); + } + }; + } + + public void setPropertyValue(Property property, Object value) { + try { + Method writeMethod = property.getWriteMethod(); + writeMethod.setAccessible(true); + writeMethod.invoke(getInstance(), value); + } + catch (Exception ex) { + throw new IllegalStateException( + "Unable to set value for property " + property.getName(), ex); + } + } + + @SuppressWarnings("unchecked") + public T getInstance() { + if (this.instance == null) { + if (this.existingValue != null) { + this.instance = this.existingValue.get(); + } + if (this.instance == null) { + this.instance = (T) BeanUtils.instantiateClass(this.type); + } + } + return this.instance; + } + + public static Bean get(Bindable bindable, + boolean useExistingValueForType) { + Class type = bindable.getType().resolve(); + Supplier value = bindable.getValue(); + if (value == null && (type.isInterface() || !hasDefaultConstructor(type))) { + return null; + } + if (useExistingValueForType && value != null) { + T instance = value.get(); + type = (instance != null ? instance.getClass() : type); + } + return new Bean<>(type, value); + } + + private static boolean hasDefaultConstructor(Class type) { + return Arrays.stream(type.getDeclaredConstructors()) + .filter((c) -> c.getParameterCount() == 0).findFirst().isPresent(); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java new file mode 100644 index 000000000000..37420371e40a --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java @@ -0,0 +1,146 @@ +/* + * Copyright 2012-2017 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; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; + +/** + * {@link AggregateBinder} for Maps. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class MapBinder extends AggregateBinder> { + + MapBinder(BindContext context) { + super(context); + } + + @Override + protected Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, Class type) { + Map map = CollectionFactory.createMap(type, 0); + for (ConfigurationPropertySource source : getContext().getSources()) { + if (!ConfigurationPropertyName.EMPTY.equals(name)) { + source = source.filter(name::isAncestorOf); + } + new EntryBinder(name, target, elementBinder).bindEntries(source, map); + } + return (map.isEmpty() ? null : map); + } + + @Override + protected Map merge(Map existing, + Map additional) { + existing.putAll(additional); + return existing; + } + + private class EntryBinder { + + private final ConfigurationPropertyName root; + + private final AggregateElementBinder elementBinder; + + private final ResolvableType mapType; + + private final ResolvableType keyType; + + private final ResolvableType valueType; + + EntryBinder(ConfigurationPropertyName root, Bindable target, + AggregateElementBinder elementBinder) { + this.root = root; + this.elementBinder = elementBinder; + this.mapType = target.getType().asMap(); + this.keyType = this.mapType.getGeneric(0); + this.valueType = this.mapType.getGeneric(1); + } + + public void bindEntries(ConfigurationPropertySource source, + Map map) { + for (ConfigurationPropertyName name : source) { + Bindable valueBindable = getValueBindable(source, name); + ConfigurationPropertyName entryName = getEntryName(source, name); + Object key = getContext().getConversionService() + .convert(getKeyName(entryName), this.keyType); + Object value = this.elementBinder.bind(entryName, valueBindable); + map.putIfAbsent(key, value); + } + } + + private Bindable getValueBindable(ConfigurationPropertySource source, + ConfigurationPropertyName name) { + if (isMultiElementName(name) && isValueTreatedAsNestedMap()) { + return Bindable.of(this.mapType); + } + return Bindable.of(this.valueType); + } + + private ConfigurationPropertyName getEntryName(ConfigurationPropertySource source, + ConfigurationPropertyName name) { + if (isMultiElementName(name) + && (isValueTreatedAsNestedMap() || !isScalarValue(source, name))) { + return rollUp(name, this.root); + } + return name; + } + + private boolean isMultiElementName(ConfigurationPropertyName name) { + return name.getParent() != null && !this.root.equals(name.getParent()); + } + + private boolean isValueTreatedAsNestedMap() { + return Object.class.equals(this.valueType.resolve(Object.class)); + } + + private boolean isScalarValue(ConfigurationPropertySource source, + ConfigurationPropertyName name) { + if (Map.class.isAssignableFrom(this.valueType.resolve()) + || Collection.class.isAssignableFrom(this.valueType.resolve()) + || this.valueType.isArray()) { + return false; + } + ConfigurationProperty property = source.getConfigurationProperty(name); + if (property == null) { + return false; + } + Object value = property.getValue(); + value = getContext().getPlaceholdersResolver().resolvePlaceholders(value); + BinderConversionService conversionService = getContext() + .getConversionService(); + return conversionService.canConvert(value, this.valueType); + } + + private String getKeyName(ConfigurationPropertyName name) { + return name.stream(this.root).map((e) -> e.getValue(Form.ORIGINAL)) + .collect(Collectors.joining(".")); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java new file mode 100644 index 000000000000..5bc2900d58f2 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2017 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; + +import org.springframework.core.env.PropertyResolver; + +/** + * Optional strategy that used by a {@link Binder} to resolve property placeholders. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see PropertySourcesPlaceholdersResolver + */ +@FunctionalInterface +public interface PlaceholdersResolver { + + /** + * No-op {@link PropertyResolver}. + */ + PlaceholdersResolver NONE = (value) -> value; + + /** + * Called to resolve any place holders in the given value. + * @param value the source value + * @return a value with place holders resolved + */ + Object resolvePlaceholders(Object value); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolver.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolver.java new file mode 100644 index 000000000000..a168d651eb3a --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolver.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2017 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; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySources; +import org.springframework.util.Assert; +import org.springframework.util.PropertyPlaceholderHelper; +import org.springframework.util.SystemPropertyUtils; + +/** + * {@link PlaceholdersResolver} to resolve placeholders from {@link PropertySources}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class PropertySourcesPlaceholdersResolver implements PlaceholdersResolver { + + private PropertySources sources; + + private PropertyPlaceholderHelper helper; + + public PropertySourcesPlaceholdersResolver(Environment environment) { + this(getSources(environment), null); + } + + public PropertySourcesPlaceholdersResolver(PropertySources sources) { + this(sources, null); + } + + public PropertySourcesPlaceholdersResolver(PropertySources sources, + PropertyPlaceholderHelper helper) { + this.sources = sources; + this.helper = (helper != null ? helper + : new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, + SystemPropertyUtils.PLACEHOLDER_SUFFIX, + SystemPropertyUtils.VALUE_SEPARATOR, false)); + } + + @Override + public Object resolvePlaceholders(Object value) { + if (value != null && value instanceof String) { + return this.helper.replacePlaceholders((String) value, + this::resolvePlaceholder); + } + return value; + } + + private String resolvePlaceholder(String placeholder) { + if (this.sources != null) { + for (PropertySource source : this.sources) { + Object value = source.getProperty(placeholder); + if (value != null) { + return String.valueOf(value); + } + } + } + return null; + } + + private static PropertySources getSources(Environment environment) { + Assert.notNull(environment, "Environment must not be null"); + Assert.isInstanceOf(ConfigurableEnvironment.class, environment, + "Environment must be a ConfigurableEnvironment"); + return ((ConfigurableEnvironment) environment).getPropertySources(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/UnboundConfigurationPropertiesException.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/UnboundConfigurationPropertiesException.java new file mode 100644 index 000000000000..62b961ecc348 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/UnboundConfigurationPropertiesException.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2017 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; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; + +/** + * {@link BindException} thrown when {@link ConfigurationPropertySource} elements were + * left unbound. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class UnboundConfigurationPropertiesException extends RuntimeException { + + private final Set unboundProperties; + + public UnboundConfigurationPropertiesException( + Set unboundProperties) { + super(buildMessage(unboundProperties)); + this.unboundProperties = Collections.unmodifiableSet(unboundProperties); + } + + public Set getUnboundProperties() { + return this.unboundProperties; + } + + private static String buildMessage(Set unboundProperties) { + StringBuilder builder = new StringBuilder(); + builder.append("The elements ["); + String message = unboundProperties.stream().map((p) -> p.getName().toString()) + .collect(Collectors.joining(",")); + builder.append(message).append("] were left unbound."); + return builder.toString(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java new file mode 100644 index 000000000000..9179579e78b9 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java @@ -0,0 +1,147 @@ +/* + * Copyright 2012-2017 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.function.Function; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.ResolvableType; +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.format.annotation.DateTimeFormat; +import org.springframework.format.datetime.DateFormatter; +import org.springframework.format.datetime.DateFormatterRegistrar; +import org.springframework.format.support.DefaultFormattingConversionService; + +/** + * Internal {@link ConversionService} used by the {@link Binder}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 2.0.0 + */ +public class BinderConversionService implements ConversionService { + + private final ConversionService conversionService; + + private final ConversionService additionalConversionService; + + /** + * Create a new {@link BinderConversionService} instance. + * @param conversionService and option root conversion service + */ + public BinderConversionService(ConversionService conversionService) { + this.conversionService = (conversionService != null ? conversionService + : new DefaultFormattingConversionService()); + this.additionalConversionService = createAdditionalConversionService(); + } + + /** + * Return {@code true} if the given source object can be converted to the + * {@code targetType}. + * @param source the source object + * @param targetType the target type to convert to (required) + * @return {@code true} if a conversion can be performed, {@code false} if not + * @throws IllegalArgumentException if {@code targetType} is {@code null} + */ + public boolean canConvert(Object source, ResolvableType targetType) { + TypeDescriptor sourceType = TypeDescriptor.forObject(source); + return canConvert(sourceType, ResolvableTypeDescriptor.forType(targetType)); + } + + @Override + public boolean canConvert(Class sourceType, Class targetType) { + return (this.conversionService != null + && this.conversionService.canConvert(sourceType, targetType)) + || this.additionalConversionService.canConvert(sourceType, targetType); + } + + @Override + public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (this.conversionService != null + && this.conversionService.canConvert(sourceType, targetType)) + || this.additionalConversionService.canConvert(sourceType, targetType); + } + + @SuppressWarnings("unchecked") + public T convert(Object value, ResolvableType type) { + TypeDescriptor sourceType = TypeDescriptor.forObject(value); + TypeDescriptor targetType = ResolvableTypeDescriptor.forType(type); + return (T) convert(value, sourceType, targetType); + } + + @SuppressWarnings("unchecked") + public T convert(Object value, Bindable bindable) { + TypeDescriptor sourceType = TypeDescriptor.forObject(value); + TypeDescriptor targetType = ResolvableTypeDescriptor.forBindable(bindable); + return (T) convert(value, sourceType, targetType); + } + + @Override + public T convert(Object source, Class 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 callConversionService(Function call) { + if (this.conversionService == null) { + return callAdditionalConversionService(call, null); + } + try { + return call.apply(this.conversionService); + } + catch (ConversionException ex) { + return callAdditionalConversionService(call, ex); + } + } + + private T callAdditionalConversionService(Function call, + RuntimeException cause) { + try { + return call.apply(this.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()); + DateFormatterRegistrar registrar = new DateFormatterRegistrar(); + DateFormatter formatter = new DateFormatter(); + formatter.setIso(DateTimeFormat.ISO.DATE_TIME); + registrar.setFormatter(formatter); + registrar.registerFormatters(service); + return service; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java new file mode 100644 index 000000000000..02a09e37ad4b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2017 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.beans.PropertyEditor; +import java.net.InetAddress; + +import org.springframework.core.convert.converter.Converter; + +/** + * {@link PropertyEditor} for {@link InetAddress} objects. + * + * @author Dave Syer + * @author Phillip Webb + */ +class InetAddressToStringConverter implements Converter { + + @Override + public String convert(InetAddress source) { + return source.getHostAddress(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java new file mode 100644 index 000000000000..977336eb7224 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2017 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.beans.PropertyEditor; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.PropertyEditorRegistrySupport; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * {@link GenericConverter} that delegates to Java bean {@link PropertyEditor + * PropertyEditors}. + * + * @author Phillip Webb + */ +class PropertyEditorConverter implements GenericConverter, ConditionalConverter { + + private static final Set> SKIPPED; + + static { + Set> skipped = new LinkedHashSet<>(); + skipped.add(Collection.class); + skipped.add(Map.class); + SKIPPED = Collections.unmodifiableSet(skipped); + } + + /** + * Registry that can be used to check if conversion is supported. Since + * {@link PropertyEditor PropertyEditors} are not thread safe this can't be used for + * actual conversion. + */ + private final PropertyEditorRegistrySupport registry = new SimpleTypeConverter(); + + @Override + public Set getConvertibleTypes() { + return null; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + Class type = targetType.getType(); + if (isSkipped(type)) { + return false; + } + PropertyEditor editor = this.registry.getDefaultEditor(type); + editor = (editor != null ? editor : BeanUtils.findEditorByConvention(type)); + return editor != null; + } + + private boolean isSkipped(Class type) { + return SKIPPED.stream().anyMatch((c) -> c.isAssignableFrom(type)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return new SimpleTypeConverter().convertIfNecessary(source, targetType.getType()); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptor.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptor.java new file mode 100644 index 000000000000..76fe621b91c5 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2017 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.Annotation; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.TypeDescriptor; + +/** + * A {@link TypeDescriptor} backed by a {@link ResolvableType}. + * + * @author Phillip Webb + */ +@SuppressWarnings("serial") +final class ResolvableTypeDescriptor extends TypeDescriptor { + + private ResolvableTypeDescriptor(ResolvableType resolvableType, + Annotation[] annotations) { + super(resolvableType, null, annotations); + } + + /** + * Create a {@link TypeDescriptor} for the specified {@link Bindable}. + * @param bindable the bindable + * @return the type descriptor + */ + public static TypeDescriptor forBindable(Bindable bindable) { + return forType(bindable.getType(), bindable.getAnnotations()); + } + + /** + * Return a {@link TypeDescriptor} for the specified {@link ResolvableType}. + * @param type the resolvable type + * @param annotations the annotations to include + * @return the type descriptor + */ + public static TypeDescriptor forType(ResolvableType type, Annotation... annotations) { + return new ResolvableTypeDescriptor(type, annotations); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java new file mode 100644 index 000000000000..67fbd690e9f2 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2017 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 org.springframework.core.convert.converter.Converter; + +/** + * Converts a String to a Char Array. + * + * @author Phillip Webb + */ +class StringToCharArrayConverter implements Converter { + + @Override + public char[] convert(String source) { + return source.toCharArray(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java new file mode 100644 index 000000000000..da9b4ab51319 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2017 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.EnumSet; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.util.Assert; + +/** + * Converts from a String to a {@link java.lang.Enum} by calling searching matching enum + * names (ignoring case). + * + * @author Phillip Webb + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +class StringToEnumConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + Class enumType = targetType; + while (enumType != null && !enumType.isEnum()) { + enumType = enumType.getSuperclass(); + } + Assert.notNull(enumType, + "The target type " + targetType.getName() + " does not refer to an enum"); + return new StringToEnum(enumType); + } + + private class StringToEnum implements Converter { + + private final Class enumType; + + StringToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(String source) { + if (source.isEmpty()) { + return null; + } + source = source.trim(); + try { + return (T) Enum.valueOf(this.enumType, source); + } + catch (Exception ex) { + return findEnum(source); + } + } + + private T findEnum(String source) { + String name = getLettersAndDigits(source); + for (T candidate : (Set) EnumSet.allOf(this.enumType)) { + if (getLettersAndDigits(candidate.name()).equals(name)) { + return candidate; + } + } + throw new IllegalArgumentException("No enum constant " + + this.enumType.getCanonicalName() + "." + source); + } + + private String getLettersAndDigits(String name) { + StringBuilder canonicalName = new StringBuilder(name.length()); + name.chars().map((c) -> (char) c).filter(Character::isLetterOrDigit) + .map(Character::toLowerCase).forEach(canonicalName::append); + return canonicalName.toString(); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java new file mode 100644 index 000000000000..9ccb0c58c771 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2017 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.beans.PropertyEditor; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.springframework.core.convert.converter.Converter; + +/** + * {@link PropertyEditor} for {@link InetAddress} objects. + * + * @author Dave Syer + * @author Phillip Webb + */ +class StringToInetAddressConverter implements Converter { + + @Override + public InetAddress convert(String source) { + try { + return InetAddress.getByName(source); + } + catch (UnknownHostException ex) { + throw new IllegalStateException("Unknown host " + source, ex); + } + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java new file mode 100644 index 000000000000..f2256e0184a7 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 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. + */ + +/** + * Conversion support for configuration properties binding. + */ +package org.springframework.boot.context.properties.bind.convert; diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandler.java new file mode 100644 index 000000000000..c0935c16247a --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2017 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.handler; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; + +/** + * {@link BindHandler} that can be used to ignore binding errors. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class IgnoreErrorsBindHandler extends AbstractBindHandler { + + public IgnoreErrorsBindHandler() { + super(); + } + + public IgnoreErrorsBindHandler(BindHandler parent) { + super(parent); + } + + @Override + public Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + return (target.getValue() == null ? null : target.getValue().get()); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandler.java new file mode 100644 index 000000000000..e5a26be0493e --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2017 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.handler; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; + +/** + * {@link BindHandler} to limit binding to only first level properties. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class IgnoreNestedPropertiesBindHandler extends AbstractBindHandler { + + public IgnoreNestedPropertiesBindHandler() { + super(); + } + + public IgnoreNestedPropertiesBindHandler(BindHandler parent) { + super(parent); + } + + @Override + public boolean onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + if (context.getDepth() > 1) { + return false; + } + return super.onStart(name, target, context); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java new file mode 100644 index 000000000000..fbb4a3fa9b68 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2017 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.handler; + +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.UnboundConfigurationPropertiesException; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; + +/** + * {@link BindHandler} to enforce that all configuration properties under the root name + * have been bound. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class NoUnboundElementsBindHandler extends AbstractBindHandler { + + private final Set boundNames = new HashSet<>(); + + public NoUnboundElementsBindHandler() { + super(); + } + + public NoUnboundElementsBindHandler(BindHandler parent) { + super(parent); + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + this.boundNames.add(name); + return super.onSuccess(name, target, context, result); + } + + @Override + public void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + if (context.getDepth() == 0) { + checkNoUnboundElements(name, context); + } + } + + private void checkNoUnboundElements(ConfigurationPropertyName name, + BindContext context) { + Set unbound = new TreeSet<>(); + for (ConfigurationPropertySource source : context.getSources()) { + ConfigurationPropertySource filtered = source + .filter((candidate) -> isUnbound(name, candidate)); + for (ConfigurationPropertyName unboundName : filtered) { + try { + unbound.add(filtered.getConfigurationProperty(unboundName)); + } + catch (Exception ex) { + } + } + } + if (!unbound.isEmpty()) { + throw new UnboundConfigurationPropertiesException(unbound); + } + } + + private boolean isUnbound(ConfigurationPropertyName name, + ConfigurationPropertyName candidate) { + return name.isAncestorOf(candidate) && !this.boundNames.contains(candidate); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/package-info.java new file mode 100644 index 000000000000..cc6bfc606f9d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2017 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. + */ + +/** + * General {@link org.springframework.boot.context.properties.bind.BindHandler + * BindHandler} implementations. + */ +package org.springframework.boot.context.properties.bind.handler; diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/package-info.java new file mode 100644 index 000000000000..8df761569665 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 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. + */ + +/** + * Support for {@code @ConfigurationProperties} binding. + */ +package org.springframework.boot.context.properties.bind; diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java new file mode 100644 index 000000000000..8ff51dc40092 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2017 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.validation; + +import org.springframework.util.Assert; + +/** + * Error thrown when validation fails during a bind operation. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see ValidationErrors + * @see ValidationBindHandler + */ +public class BindValidationException extends RuntimeException { + + private final ValidationErrors validationErrors; + + BindValidationException(ValidationErrors validationErrors) { + Assert.notNull(validationErrors, "ValidationErrors must not be null"); + this.validationErrors = validationErrors; + } + + /** + * Return the validation errors that caused the exception. + * @return the validationErrors the validation errors + */ + public ValidationErrors getValidationErrors() { + return this.validationErrors; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldError.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldError.java new file mode 100644 index 000000000000..4a4643226c13 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldError.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2017 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.validation; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; +import org.springframework.validation.FieldError; + +/** + * {@link FieldError} implementation that tracks the source {@link Origin}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +final class OriginTrackedFieldError extends FieldError implements OriginProvider { + + private final Origin origin; + + private OriginTrackedFieldError(FieldError fieldError, Origin origin) { + super(fieldError.getObjectName(), fieldError.getField(), + fieldError.getRejectedValue(), fieldError.isBindingFailure(), + fieldError.getCodes(), fieldError.getArguments(), + fieldError.getDefaultMessage()); + this.origin = origin; + } + + @Override + public Origin getOrigin() { + return this.origin; + } + + @Override + public String toString() { + if (this.origin == null) { + return toString(); + } + return super.toString() + "; origin " + this.origin; + } + + public static FieldError of(FieldError fieldError, Origin origin) { + if (fieldError == null || origin == null) { + return fieldError; + } + return new OriginTrackedFieldError(fieldError, origin); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java new file mode 100644 index 000000000000..c1a64269d728 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-2017 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.validation; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; + +/** + * {@link BindHandler} to apply {@link Validator Validators} to bound results. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class ValidationBindHandler extends AbstractBindHandler { + + private final Validator[] validators; + + private boolean validate; + + private Set boundProperties = new LinkedHashSet<>(); + + public ValidationBindHandler(Validator... validators) { + super(); + this.validators = validators; + } + + public ValidationBindHandler(BindHandler parent, Validator... validators) { + super(parent); + this.validators = validators; + } + + @Override + public boolean onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + if (context.getDepth() == 0) { + this.validate = shouldValidate(target); + } + return super.onStart(name, target, context); + } + + private boolean shouldValidate(Bindable target) { + Validated annotation = AnnotationUtils + .findAnnotation(target.getBoxedType().resolve(), Validated.class); + return (annotation != null); + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + if (context.getConfigurationProperty() != null) { + this.boundProperties.add(context.getConfigurationProperty()); + } + return super.onSuccess(name, target, context, result); + } + + @Override + public void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + if (this.validate) { + validate(name, target, result); + } + super.onFinish(name, target, context, result); + } + + private void validate(ConfigurationPropertyName name, Bindable target, + Object result) { + Object validationTarget = getValidationTarget(target, result); + Class validationType = target.getBoxedType().resolve(); + validate(name, validationTarget, validationType); + } + + private Object getValidationTarget(Bindable target, Object result) { + if (result != null) { + return result; + } + if (target.getValue() != null) { + return target.getValue().get(); + } + return null; + } + + private void validate(ConfigurationPropertyName name, Object target, Class type) { + if (target != null) { + BindingResult errors = new BeanPropertyBindingResult(target, name.toString()); + Arrays.stream(this.validators).filter((v) -> v.supports(type)) + .forEach((v) -> v.validate(target, errors)); + if (errors.hasErrors()) { + throwBindValidationException(name, errors); + } + } + } + + private void throwBindValidationException(ConfigurationPropertyName name, + BindingResult errors) { + Set boundProperties = this.boundProperties.stream() + .filter((property) -> name.isAncestorOf(property.getName())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + ValidationErrors validationErrors = new ValidationErrors(name, boundProperties, + errors.getAllErrors()); + throw new BindValidationException(validationErrors); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java new file mode 100644 index 000000000000..7001fb05c8d3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2017 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.validation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.assertj.core.util.Objects; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; +import org.springframework.boot.origin.Origin; +import org.springframework.util.Assert; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +/** + * A collection of {@link ObjectError ObjectErrors} caused by bind validation failures. + * Where possible, included {@link FieldError FieldErrors} will be OriginProvider. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class ValidationErrors implements Iterable { + + private final ConfigurationPropertyName name; + + private final Set boundProperties; + + private final List errors; + + ValidationErrors(ConfigurationPropertyName name, + Set boundProperties, List errors) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(boundProperties, "BoundProperties must not be null"); + Assert.notNull(errors, "Errors must not be null"); + this.name = name; + this.boundProperties = Collections.unmodifiableSet(boundProperties); + this.errors = convertErrors(name, boundProperties, errors); + } + + private List convertErrors(ConfigurationPropertyName name, + Set boundProperties, List errors) { + List converted = new ArrayList<>(errors.size()); + for (ObjectError error : errors) { + converted.add(convertError(name, boundProperties, error)); + } + return Collections.unmodifiableList(converted); + } + + private ObjectError convertError(ConfigurationPropertyName name, + Set boundProperties, ObjectError error) { + if (error instanceof FieldError) { + return convertFieldError(name, boundProperties, (FieldError) error); + } + return error; + } + + private FieldError convertFieldError(ConfigurationPropertyName name, + Set boundProperties, FieldError error) { + if (error instanceof ObjectProvider) { + return error; + } + return OriginTrackedFieldError.of(error, + findFieldErrorOrigin(name, boundProperties, error)); + } + + private Origin findFieldErrorOrigin(ConfigurationPropertyName name, + Set boundProperties, FieldError error) { + for (ConfigurationProperty boundProperty : boundProperties) { + if (isForError(name, boundProperty.getName(), error)) { + return Origin.from(boundProperty); + } + } + return null; + } + + private boolean isForError(ConfigurationPropertyName name, + ConfigurationPropertyName boundPropertyName, FieldError error) { + return Objects.areEqual(boundPropertyName.getParent(), name) && boundPropertyName + .getElement().getValue(Form.UNIFORM).equalsIgnoreCase(error.getField()); + } + + /** + * Return the name of the item that was being validated. + * @return the name of the item + */ + public ConfigurationPropertyName getName() { + return this.name; + } + + /** + * Return the properties that were bound before validation failed. + * @return the boundProperties + */ + public Set getBoundProperties() { + return this.boundProperties; + } + + public boolean hasErrors() { + return !this.errors.isEmpty(); + } + + /** + * Return the list of all validation errors. + * @return the errors + */ + public List getAllErrors() { + return this.errors; + } + + @Override + public Iterator iterator() { + return this.errors.iterator(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/package-info.java new file mode 100644 index 000000000000..fdc8cc8436eb --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 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. + */ + +/** + * Binding validation support. + */ +package org.springframework.boot.context.properties.bind.validation; diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java new file mode 100644 index 000000000000..3510c971a100 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java @@ -0,0 +1,274 @@ +/* + * Copyright 2012-2017 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; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.InOrder; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.core.ResolvableType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link ArrayBinder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ArrayBinderTests { + + private static final Bindable> INTEGER_LIST = Bindable + .listOf(Integer.class); + + private static final Bindable INTEGER_ARRAY = Bindable.of(Integer[].class); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void bindToArrayShouldReturnArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "1"); + source.put("foo[1]", "2"); + source.put("foo[2]", "3"); + this.sources.add(source); + Integer[] result = this.binder.bind("foo", INTEGER_ARRAY).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToCollectionShouldTriggerOnSuccess() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo[0]", "1", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + this.binder.bind("foo", INTEGER_LIST, handler); + InOrder inOrder = inOrder(handler); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo[0]")), + eq(Bindable.of(Integer.class)), any(), eq(1)); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(INTEGER_LIST), any(), isA(List.class)); + } + + @Test + public void bindToArrayShouldReturnPrimativeArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "1"); + source.put("foo[1]", "2"); + source.put("foo[2]", "3"); + this.sources.add(source); + int[] result = this.binder.bind("foo", Bindable.of(int[].class)).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToArrayWhenNestedShouldReturnPopulatedArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0][0]", "1"); + source.put("foo[0][1]", "2"); + source.put("foo[1][0]", "3"); + source.put("foo[1][1]", "4"); + this.sources.add(source); + ResolvableType type = ResolvableType.forArrayComponent(INTEGER_ARRAY.getType()); + Bindable target = Bindable.of(type); + Integer[][] result = this.binder.bind("foo", target).get(); + assertThat(result).hasSize(2); + assertThat(result[0]).containsExactly(1, 2); + assertThat(result[1]).containsExactly(3, 4); + } + + @Test + public void bindToArrayWhenNestedListShouldReturnPopulatedArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0][0]", "1"); + source.put("foo[0][1]", "2"); + source.put("foo[1][0]", "3"); + source.put("foo[1][1]", "4"); + this.sources.add(source); + ResolvableType type = ResolvableType.forArrayComponent(INTEGER_LIST.getType()); + Bindable[]> target = Bindable.of(type); + List[] result = this.binder.bind("foo", target).get(); + assertThat(result).hasSize(2); + assertThat(result[0]).containsExactly(1, 2); + assertThat(result[1]).containsExactly(3, 4); + } + + @Test + public void bindToArrayWhenNotInOrderShouldReturnPopulatedArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[1]", "2"); + source.put("foo[0]", "1"); + source.put("foo[2]", "3"); + this.sources.add(source); + Integer[] result = this.binder.bind("foo", INTEGER_ARRAY).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToArrayWhenNonSequentialShouldThrowException() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "2"); + source.put("foo[1]", "1"); + source.put("foo[3]", "3"); + this.sources.add(source); + try { + this.binder.bind("foo", INTEGER_ARRAY); + fail("No exception thrown"); + } + catch (BindException ex) { + Set unbound = ((UnboundConfigurationPropertiesException) ex + .getCause()).getUnboundProperties(); + assertThat(unbound.size()).isEqualTo(1); + ConfigurationProperty property = unbound.iterator().next(); + assertThat(property.getName().toString()).isEqualTo("foo[3]"); + assertThat(property.getValue()).isEqualTo("3"); + } + } + + @Test + public void bindToArrayWhenNonIterableShouldReturnPopulatedArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[1]", "2"); + source.put("foo[0]", "1"); + source.put("foo[2]", "3"); + source.setNonIterable(true); + this.sources.add(source); + Integer[] result = this.binder.bind("foo", INTEGER_ARRAY).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToArrayWhenMultipleSourceShouldOnlyUseFirst() throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("bar", "baz"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo[0]", "1"); + source2.put("foo[1]", "2"); + this.sources.add(source2); + MockConfigurationPropertySource source3 = new MockConfigurationPropertySource(); + source3.put("foo[0]", "7"); + source3.put("foo[1]", "8"); + source3.put("foo[2]", "9"); + this.sources.add(source3); + Integer[] result = this.binder.bind("foo", INTEGER_ARRAY).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToArrayWhenHasExistingCollectionShouldReplaceAllContents() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo[0]", "1")); + Integer[] existing = new Integer[2]; + existing[0] = 1000; + existing[1] = 1001; + Integer[] result = this.binder + .bind("foo", INTEGER_ARRAY.withExistingValue(existing)).get(); + assertThat(result).containsExactly(1); + } + + @Test + public void bindToArrayWhenNoValueShouldReturnUnbound() throws Exception { + this.sources.add(new MockConfigurationPropertySource("faf.bar", "1")); + BindResult result = this.binder.bind("foo", INTEGER_ARRAY); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToArrayShouldTriggerOnSuccess() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo[0]", "1", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + Bindable target = INTEGER_ARRAY; + this.binder.bind("foo", target, handler); + InOrder inOrder = inOrder(handler); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo[0]")), + eq(Bindable.of(Integer.class)), any(), eq(1)); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(target), any(), isA(Integer[].class)); + } + + @Test + public void bindToArrayWhenCommaListShouldReturnPopulatedArray() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "1,2,3")); + int[] result = this.binder.bind("foo", Bindable.of(int[].class)).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToArrayWhenCommaListAndIndexedShouldOnlyUseFirst() throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("foo", "1,2"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo[0]", "2"); + source2.put("foo[1]", "3"); + int[] result = this.binder.bind("foo", Bindable.of(int[].class)).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToArrayWhenIndexedAndCommaListShouldOnlyUseFirst() throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("foo[0]", "1"); + source1.put("foo[1]", "2"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo", "2,3"); + int[] result = this.binder.bind("foo", Bindable.of(int[].class)).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToArrayShouldBindCharArray() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "word")); + char[] result = this.binder.bind("foo", Bindable.of(char[].class)).get(); + assertThat(result).containsExactly("word".toCharArray()); + } + + @Test + public void bindToArrayWhenEmptyStringShouldReturnEmptyArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo", ""); + this.sources.add(source); + String[] result = this.binder.bind("foo", Bindable.of(String[].class)).get(); + assertThat(result).isNotNull().isEmpty(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java new file mode 100644 index 000000000000..fa05fc4204ec --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2017 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; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BeanPropertyName}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class BeanPropertyNameTests { + + @Test + public void toDashedCaseShouldConvertValue() { + assertThat(BeanPropertyName.toDashedForm("Foo")).isEqualTo("foo"); + assertThat(BeanPropertyName.toDashedForm("foo")).isEqualTo("foo"); + assertThat(BeanPropertyName.toDashedForm("fooBar")).isEqualTo("foo-bar"); + assertThat(BeanPropertyName.toDashedForm("foo_bar")).isEqualTo("foo-bar"); + assertThat(BeanPropertyName.toDashedForm("_foo_bar")).isEqualTo("-foo-bar"); + assertThat(BeanPropertyName.toDashedForm("foo_Bar")).isEqualTo("foo-bar"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java new file mode 100644 index 000000000000..1c4a67a68f19 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java @@ -0,0 +1,234 @@ +/* + * Copyright 2012-2017 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; + +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link BindResult}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class BindResultTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Mock + private Consumer consumer; + + @Mock + private Function mapper; + + @Mock + private Supplier supplier; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void getWhenHasValueShouldReturnValue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.get()).isEqualTo("foo"); + } + + @Test + public void getWhenHasNoValueShouldThrowException() throws Exception { + BindResult result = BindResult.of(null); + this.thrown.expect(NoSuchElementException.class); + this.thrown.expectMessage("No value bound"); + result.get(); + } + + @Test + public void isBoundWhenHasValueShouldReturnTrue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.isBound()).isTrue(); + } + + @Test + public void isBoundWhenHasNoValueShouldFalse() throws Exception { + BindResult result = BindResult.of(null); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void ifBoundWhenConsumerIsNullShouldThrowException() throws Exception { + BindResult result = BindResult.of("foo"); + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Consumer must not be null"); + result.ifBound(null); + } + + @Test + public void ifBoundWhenHasValueShouldCallConsumer() throws Exception { + BindResult result = BindResult.of("foo"); + result.ifBound(this.consumer); + verify(this.consumer).accept("foo"); + } + + @Test + public void ifBoundWhenHasNoValueShouldNotCallConsumer() throws Exception { + BindResult result = BindResult.of(null); + result.ifBound(this.consumer); + verifyZeroInteractions(this.consumer); + } + + @Test + public void mapWhenMapperIsNullShouldThrowException() throws Exception { + BindResult result = BindResult.of("foo"); + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Mapper must not be null"); + result.map(null); + } + + @Test + public void mapWhenHasValueShouldCallMapper() throws Exception { + BindResult result = BindResult.of("foo"); + given(this.mapper.apply("foo")).willReturn("bar"); + assertThat(result.map(this.mapper).get()).isEqualTo("bar"); + } + + @Test + public void mapWhenHasNoValueShouldNotCallMapper() throws Exception { + BindResult result = BindResult.of(null); + result.map(this.mapper); + verifyZeroInteractions(this.mapper); + } + + @Test + public void orElseWhenHasValueShouldReturnValue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.orElse("bar")).isEqualTo("foo"); + } + + @Test + public void orElseWhenHasValueNoShouldReturnOther() throws Exception { + BindResult result = BindResult.of(null); + assertThat(result.orElse("bar")).isEqualTo("bar"); + } + + @Test + public void orElseGetWhenHasValueShouldReturnValue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.orElseGet(this.supplier)).isEqualTo("foo"); + verifyZeroInteractions(this.supplier); + } + + @Test + public void orElseGetWhenHasValueNoShouldReturnOther() throws Exception { + BindResult result = BindResult.of(null); + given(this.supplier.get()).willReturn("bar"); + assertThat(result.orElseGet(this.supplier)).isEqualTo("bar"); + } + + @Test + public void orElseCreateWhenTypeIsNullShouldThrowException() throws Exception { + BindResult result = BindResult.of("foo"); + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Type must not be null"); + result.orElseCreate(null); + } + + @Test + public void orElseCreateWhenHasValueShouldReturnValue() throws Exception { + BindResult result = BindResult.of(new ExampleBean("foo")); + assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("foo"); + } + + @Test + public void orElseCreateWhenHasValueNoShouldReturnCreatedValue() throws Exception { + BindResult result = BindResult.of(null); + assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("new"); + } + + @Test + public void orElseThrowWhenHasValueShouldReturnValue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.orElseThrow(IOException::new)).isEqualTo("foo"); + } + + @Test + public void orElseThrowWhenHasNoValueShouldThrowException() throws Exception { + BindResult result = BindResult.of(null); + this.thrown.expect(IOException.class); + result.orElseThrow(IOException::new); + } + + @Test + public void hashCodeAndEquals() throws Exception { + BindResult result1 = BindResult.of("foo"); + BindResult result2 = BindResult.of("foo"); + BindResult result3 = BindResult.of("bar"); + BindResult result4 = BindResult.of(null); + assertThat(result1.hashCode()).isEqualTo(result2.hashCode()); + assertThat(result1).isEqualTo(result1).isEqualTo(result2).isNotEqualTo(result3) + .isNotEqualTo(result4); + } + + @Test + public void ofWhenHasValueShouldReturnBoundResultOfValue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.isBound()).isTrue(); + assertThat(result.get()).isEqualTo("foo"); + } + + @Test + public void ofWhenValueIsNullShouldReturnUnbound() throws Exception { + BindResult result = BindResult.of(null); + assertThat(result.isBound()).isFalse(); + assertThat(result).isSameAs(BindResult.of(null)); + } + + static class ExampleBean { + + private final String value; + + ExampleBean() { + this.value = "new"; + } + + ExampleBean(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java new file mode 100644 index 000000000000..6ef83cd5c192 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2012-2017 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; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link Bindable}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class BindableTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void ofClassWhenTypeIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Type must not be null"); + Bindable.of((Class) null); + } + + @Test + public void ofTypeWhenTypeIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Type must not be null"); + Bindable.of((ResolvableType) null); + } + + @Test + public void ofClassShouldSetType() throws Exception { + assertThat(Bindable.of(String.class).getType()) + .isEqualTo(ResolvableType.forClass(String.class)); + } + + @Test + public void ofTypeShouldSetType() throws Exception { + ResolvableType type = ResolvableType.forClass(String.class); + assertThat(Bindable.of(type).getType()).isEqualTo(type); + } + + @Test + public void ofInstanceShouldSetTypeAndExistingValue() throws Exception { + String instance = "foo"; + ResolvableType type = ResolvableType.forClass(String.class); + assertThat(Bindable.ofInstance(instance).getType()).isEqualTo(type); + assertThat(Bindable.ofInstance(instance).getValue().get()).isEqualTo("foo"); + } + + @Test + public void ofClassWithExistingValueShouldSetTypeAndExistingValue() throws Exception { + assertThat(Bindable.of(String.class).withExistingValue("foo").getValue().get()) + .isEqualTo("foo"); + } + + @Test + public void ofTypeWithExistingValueShouldSetTypeAndExistingValue() throws Exception { + assertThat(Bindable.of(ResolvableType.forClass(String.class)) + .withExistingValue("foo").getValue().get()).isEqualTo("foo"); + } + + @Test + public void ofTypeWhenExistingValueIsNotInstanceOfTypeShouldThrowException() + throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage( + "ExistingValue must be an instance of " + String.class.getName()); + Bindable.of(ResolvableType.forClass(String.class)).withExistingValue(123); + } + + @Test + public void ofTypeWhenPrimitiveWithExistingValueWrapperShouldNotThrowException() + throws Exception { + Bindable bindable = Bindable + .of(ResolvableType.forClass(int.class)).withExistingValue(123); + assertThat(bindable.getType().resolve()).isEqualTo(int.class); + assertThat(bindable.getValue().get()).isEqualTo(123); + } + + @Test + public void getBoxedTypeWhenNotBoxedShouldReturnType() throws Exception { + Bindable bindable = Bindable.of(String.class); + assertThat(bindable.getBoxedType()) + .isEqualTo(ResolvableType.forClass(String.class)); + } + + @Test + public void getBoxedTypeWhenPrimativeShouldReturnBoxedType() throws Exception { + Bindable bindable = Bindable.of(int.class); + assertThat(bindable.getType()).isEqualTo(ResolvableType.forClass(int.class)); + assertThat(bindable.getBoxedType()) + .isEqualTo(ResolvableType.forClass(Integer.class)); + } + + @Test + public void getBoxedTypeWhenPrimativeArrayShouldReturnBoxedType() throws Exception { + Bindable bindable = Bindable.of(int[].class); + assertThat(bindable.getType().getComponentType()) + .isEqualTo(ResolvableType.forClass(int.class)); + assertThat(bindable.getBoxedType().isArray()).isTrue(); + assertThat(bindable.getBoxedType().getComponentType()) + .isEqualTo(ResolvableType.forClass(Integer.class)); + } + + @Test + public void getAnnotationsShouldReturnEmptyArray() throws Exception { + assertThat(Bindable.of(String.class).getAnnotations()).isEmpty(); + } + + @Test + public void withAnnotationsShouldSetAnnotations() throws Exception { + Annotation annotation = mock(Annotation.class); + assertThat(Bindable.of(String.class).withAnnotations(annotation).getAnnotations()) + .containsExactly(annotation); + } + + @Test + public void toStringShouldShowDetails() throws Exception { + Annotation annotation = AnnotationUtils + .synthesizeAnnotation(TestAnnotation.class); + Bindable bindable = Bindable.of(String.class).withExistingValue("foo") + .withAnnotations(annotation); + System.out.println(bindable.toString()); + assertThat(bindable.toString()).contains("type = java.lang.String, " + + "value = 'provided', annotations = array[" + + "@org.springframework.boot.context.properties.bind." + + "BindableTests$TestAnnotation()]"); + } + + @Test + public void equalsAndHashcode() throws Exception { + Annotation annotation = AnnotationUtils + .synthesizeAnnotation(TestAnnotation.class); + Bindable bindable1 = Bindable.of(String.class).withExistingValue("foo") + .withAnnotations(annotation); + Bindable bindable2 = Bindable.of(String.class).withExistingValue("foo") + .withAnnotations(annotation); + Bindable bindable3 = Bindable.of(String.class).withExistingValue("fof") + .withAnnotations(annotation); + assertThat(bindable1.hashCode()).isEqualTo(bindable2.hashCode()); + assertThat(bindable1).isEqualTo(bindable1).isEqualTo(bindable2); + assertThat(bindable1).isEqualTo(bindable3); + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation { + + } + + static class TestNewInstance { + + private String foo = "hello world"; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + } + + static class TestNewInstanceWithNoDefaultConstructor { + + TestNewInstanceWithNoDefaultConstructor(String foo) { + this.foo = foo; + } + + private String foo = "hello world"; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java new file mode 100644 index 000000000000..9706f6e0672d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java @@ -0,0 +1,223 @@ +/* + * Copyright 2012-2017 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; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Answers; +import org.mockito.InOrder; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.test.context.support.TestPropertySourceUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link Binder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class BinderTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void createWhenSourcesIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Sources must not be null"); + new Binder((Iterable) null); + } + + @Test + public void bindWhenNameIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be null"); + this.binder.bind((ConfigurationPropertyName) null, Bindable.of(String.class), + BindHandler.DEFAULT); + } + + @Test + public void bindWhenTargetIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Target must not be null"); + this.binder.bind(ConfigurationPropertyName.of("foo"), null, BindHandler.DEFAULT); + } + + @Test + public void bindToValueWhenPropertyIsMissingShouldReturnUnbound() throws Exception { + this.sources.add(new MockConfigurationPropertySource()); + BindResult result = this.binder.bind("foo", Bindable.of(String.class)); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToValueShouldReturnPropertyValue() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", 123)); + BindResult result = this.binder.bind("foo", Bindable.of(Integer.class)); + assertThat(result.get()).isEqualTo(123); + } + + @Test + public void bindToValueShouldReturnPropertyValueFromSecondSource() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", 123)); + this.sources.add(new MockConfigurationPropertySource("bar", 234)); + BindResult result = this.binder.bind("bar", Bindable.of(Integer.class)); + assertThat(result.get()).isEqualTo(234); + } + + @Test + public void bindToValueShouldReturnConvertedPropertyValue() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "123")); + BindResult result = this.binder.bind("foo", Bindable.of(Integer.class)); + assertThat(result.get()).isEqualTo(123); + } + + @Test + public void bindToValueWhenMultipleCandidatesShouldReturnFirst() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", 123)); + this.sources.add(new MockConfigurationPropertySource("foo", 234)); + BindResult result = this.binder.bind("foo", Bindable.of(Integer.class)); + assertThat(result.get()).isEqualTo(123); + } + + @Test + public void bindToValueWithPlaceholdersShouldResolve() throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, "bar=23"); + this.sources.add(new MockConfigurationPropertySource("foo", "1${bar}")); + this.binder = new Binder(this.sources, + new PropertySourcesPlaceholdersResolver(environment)); + BindResult result = this.binder.bind("foo", Bindable.of(Integer.class)); + assertThat(result.get()).isEqualTo(123); + } + + @Test + public void bindToValueShouldTriggerOnSuccess() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "1", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + Bindable target = Bindable.of(Integer.class); + this.binder.bind("foo", target, handler); + InOrder ordered = inOrder(handler); + ordered.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(target), any(), eq(1)); + } + + @Test + public void bindToJavaBeanShouldReturnPopulatedBean() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.value", "bar")); + JavaBean result = this.binder.bind("foo", Bindable.of(JavaBean.class)).get(); + assertThat(result.getValue()).isEqualTo("bar"); + } + + @Test + public void bindToJavaBeanWhenNonIterableShouldReturnPopulatedBean() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource( + "foo.value", "bar"); + source.setNonIterable(true); + this.sources.add(source); + JavaBean result = this.binder.bind("foo", Bindable.of(JavaBean.class)).get(); + assertThat(result.getValue()).isEqualTo("bar"); + } + + @Test + public void bindToJavaBeanShouldTriggerOnSuccess() throws Exception { + this.sources + .add(new MockConfigurationPropertySource("foo.value", "bar", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + Bindable target = Bindable.of(JavaBean.class); + this.binder.bind("foo", target, handler); + InOrder inOrder = inOrder(handler); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo.value")), + eq(Bindable.of(String.class)), any(), eq("bar")); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(target), any(), isA(JavaBean.class)); + } + + @Test + public void bindWhenHasMalformedDateShouldThrowException() throws Exception { + this.thrown.expectCause(instanceOf(ConversionFailedException.class)); + this.sources.add(new MockConfigurationPropertySource("foo", "2014-04-01")); + this.binder.bind("foo", Bindable.of(LocalDate.class)); + } + + @Test + public void bindWhenHasAnnotationsShouldChangeConvertedValue() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "2014-04-01")); + DateTimeFormat annotation = AnnotationUtils.synthesizeAnnotation( + Collections.singletonMap("iso", DateTimeFormat.ISO.DATE), + DateTimeFormat.class, null); + LocalDate result = this.binder + .bind("foo", Bindable.of(LocalDate.class).withAnnotations(annotation)) + .get(); + assertThat(result.toString()).isEqualTo("2014-04-01"); + } + + public static class JavaBean { + + private String value; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + } + + public enum ExampleEnum { + + FOO_BAR, BAR_BAZ, BAZ_BOO + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java new file mode 100644 index 000000000000..9fbef4922e0b --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java @@ -0,0 +1,264 @@ +/* + * Copyright 2012-2017 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; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.test.context.support.TestPropertySourceUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link CollectionBinder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class CollectionBinderTests { + + private static final Bindable> INTEGER_LIST = Bindable + .listOf(Integer.class); + + private static final Bindable> STRING_LIST = Bindable + .listOf(String.class); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void bindToCollectionShouldReturnPopulatedCollection() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "1"); + source.put("foo[1]", "2"); + source.put("foo[2]", "3"); + this.sources.add(source); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToCollectionWhenNestedShouldReturnPopulatedCollection() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0][0]", "1"); + source.put("foo[0][1]", "2"); + source.put("foo[1][0]", "3"); + source.put("foo[1][1]", "4"); + this.sources.add(source); + Bindable>> target = Bindable.of( + ResolvableType.forClassWithGenerics(List.class, INTEGER_LIST.getType())); + List> result = this.binder.bind("foo", target).get(); + assertThat(result).hasSize(2); + assertThat(result.get(0)).containsExactly(1, 2); + assertThat(result.get(1)).containsExactly(3, 4); + } + + @Test + public void bindToCollectionWhenNotInOrderShouldReturnPopulatedCollection() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[1]", "2"); + source.put("foo[0]", "1"); + source.put("foo[2]", "3"); + this.sources.add(source); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToCollectionWhenNonSequentialShouldThrowException() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "2"); + source.put("foo[1]", "1"); + source.put("foo[3]", "3"); + this.sources.add(source); + try { + this.binder.bind("foo", INTEGER_LIST); + fail("No exception thrown"); + } + catch (BindException ex) { + ex.printStackTrace(); + Set unbound = ((UnboundConfigurationPropertiesException) ex + .getCause()).getUnboundProperties(); + assertThat(unbound).hasSize(1); + ConfigurationProperty property = unbound.iterator().next(); + assertThat(property.getName().toString()).isEqualTo("foo[3]"); + assertThat(property.getValue()).isEqualTo("3"); + } + } + + @Test + public void bindToCollectionWhenNonIterableShouldReturnPopulatedCollection() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[1]", "2"); + source.put("foo[0]", "1"); + source.put("foo[2]", "3"); + source.setNonIterable(true); + this.sources.add(source); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToCollectionWhenMultipleSourceShouldOnlyUseFirst() throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("bar", "baz"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo[0]", "1"); + source2.put("foo[1]", "2"); + this.sources.add(source2); + MockConfigurationPropertySource source3 = new MockConfigurationPropertySource(); + source3.put("foo[0]", "7"); + source3.put("foo[1]", "8"); + source3.put("foo[2]", "9"); + this.sources.add(source3); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToCollectionWhenHasExistingCollectionShouldReplaceAllContents() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo[0]", "1")); + List existing = new LinkedList<>(); + existing.add(1000); + existing.add(1001); + List result = this.binder + .bind("foo", INTEGER_LIST.withExistingValue(existing)).get(); + assertThat(result).isExactlyInstanceOf(LinkedList.class); + assertThat(result).isSameAs(existing); + assertThat(result).containsExactly(1); + } + + @Test + public void bindToCollectionWhenHasExistingCollectionButNoValueShouldReturnUnbound() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("faf[0]", "1")); + List existing = new LinkedList<>(); + existing.add(1000); + BindResult> result = this.binder.bind("foo", + INTEGER_LIST.withExistingValue(existing)); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToCollectionShouldRespectCollectionType() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo[0]", "1")); + ResolvableType type = ResolvableType.forClassWithGenerics(LinkedList.class, + Integer.class); + Object defaultList = this.binder.bind("foo", INTEGER_LIST).get(); + Object customList = this.binder.bind("foo", Bindable.of(type)).get(); + assertThat(customList).isExactlyInstanceOf(LinkedList.class) + .isNotInstanceOf(defaultList.getClass()); + } + + @Test + public void bindToCollectionWhenNoValueShouldReturnUnbound() throws Exception { + this.sources.add(new MockConfigurationPropertySource("faf.bar", "1")); + BindResult> result = this.binder.bind("foo", INTEGER_LIST); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToCollectionWhenCommaListShouldReturnPopulatedCollection() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "1,2,3")); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToCollectionWhenCommaListWithPlaceholdersShouldReturnPopulatedCollection() + throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, + "bar=1,2,3"); + this.binder = new Binder(this.sources, + new PropertySourcesPlaceholdersResolver(environment)); + this.sources.add(new MockConfigurationPropertySource("foo", "${bar}")); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2, 3); + + } + + @Test + public void bindToCollectionWhenCommaListAndIndexedShouldOnlyUseFirst() + throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("foo", "1,2"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo[0]", "2"); + source2.put("foo[1]", "3"); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToCollectionWhenIndexedAndCommaListShouldOnlyUseFirst() + throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("foo[0]", "1"); + source1.put("foo[1]", "2"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo", "2,3"); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToCollectionWhenItemContainsCommasShouldReturnPopulatedCollection() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "1,2"); + source.put("foo[1]", "3"); + this.sources.add(source); + List result = this.binder.bind("foo", STRING_LIST).get(); + assertThat(result).containsExactly("1,2", "3"); + } + + @Test + public void bindToCollectionWhenEmptyStringShouldReturnEmptyCollection() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo", ""); + this.sources.add(source); + List result = this.binder.bind("foo", STRING_LIST).get(); + assertThat(result).isNotNull().isEmpty(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java new file mode 100644 index 000000000000..3bc3f73ed462 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java @@ -0,0 +1,810 @@ +/* + * Copyright 2012-2017 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; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +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; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.format.annotation.DateTimeFormat; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link JavaBeanBinder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class JavaBeanBinderTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void bindToClassShouldCreateBoundBean() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "12"); + source.put("foo.long-value", "34"); + source.put("foo.string-value", "foo"); + source.put("foo.enum-value", "foo-bar"); + this.sources.add(source); + ExampleValueBean bean = this.binder + .bind("foo", Bindable.of(ExampleValueBean.class)).get(); + assertThat(bean.getIntValue()).isEqualTo(12); + assertThat(bean.getLongValue()).isEqualTo(34); + assertThat(bean.getStringValue()).isEqualTo("foo"); + assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); + } + + @Test + public void bindToClassWhenHasNoPrefixShouldCreateBoundBean() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("int-value", "12"); + source.put("long-value", "34"); + source.put("string-value", "foo"); + source.put("enum-value", "foo-bar"); + this.sources.add(source); + ExampleValueBean bean = this.binder.bind(ConfigurationPropertyName.of(""), + Bindable.of(ExampleValueBean.class)).get(); + assertThat(bean.getIntValue()).isEqualTo(12); + assertThat(bean.getLongValue()).isEqualTo(34); + assertThat(bean.getStringValue()).isEqualTo("foo"); + assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); + } + + @Test + public void bindToInstanceShouldBindToInstance() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "12"); + source.put("foo.long-value", "34"); + source.put("foo.string-value", "foo"); + source.put("foo.enum-value", "foo-bar"); + this.sources.add(source); + ExampleValueBean bean = new ExampleValueBean(); + ExampleValueBean boundBean = this.binder + .bind("foo", Bindable.of(ExampleValueBean.class).withExistingValue(bean)) + .get(); + assertThat(boundBean).isSameAs(bean); + assertThat(bean.getIntValue()).isEqualTo(12); + assertThat(bean.getLongValue()).isEqualTo(34); + assertThat(bean.getStringValue()).isEqualTo("foo"); + assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); + } + + @Test + public void bindToInstanceWithNoPropertiesShouldReturnUnbound() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + this.sources.add(source); + ExampleDefaultsBean bean = new ExampleDefaultsBean(); + BindResult boundBean = this.binder.bind("foo", + Bindable.of(ExampleDefaultsBean.class).withExistingValue(bean)); + assertThat(boundBean.isBound()).isFalse(); + assertThat(bean.getFoo()).isEqualTo(123); + assertThat(bean.getBar()).isEqualTo(456); + } + + @Test + public void bindToClassShouldLeaveDefaults() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "999"); + this.sources.add(source); + ExampleDefaultsBean bean = this.binder + .bind("foo", Bindable.of(ExampleDefaultsBean.class)).get(); + assertThat(bean.getFoo()).isEqualTo(123); + assertThat(bean.getBar()).isEqualTo(999); + } + + @Test + public void bindToExistingInstanceShouldLeaveDefaults() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "999"); + this.sources.add(source); + ExampleDefaultsBean bean = new ExampleDefaultsBean(); + bean.setFoo(888); + ExampleDefaultsBean boundBean = this.binder + .bind("foo", + Bindable.of(ExampleDefaultsBean.class).withExistingValue(bean)) + .get(); + assertThat(boundBean).isSameAs(bean); + assertThat(bean.getFoo()).isEqualTo(888); + assertThat(bean.getBar()).isEqualTo(999); + } + + @Test + public void bindToClassShouldBindToMap() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.map.foo-bar", "1"); + source.put("foo.map.bar-baz", "2"); + this.sources.add(source); + ExampleMapBean bean = this.binder.bind("foo", Bindable.of(ExampleMapBean.class)) + .get(); + assertThat(bean.getMap()).containsExactly(entry(ExampleEnum.FOO_BAR, 1), + entry(ExampleEnum.BAR_BAZ, 2)); + } + + @Test + public void bindToClassShouldBindToList() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.list[0]", "foo-bar"); + source.put("foo.list[1]", "bar-baz"); + this.sources.add(source); + ExampleListBean bean = this.binder.bind("foo", Bindable.of(ExampleListBean.class)) + .get(); + assertThat(bean.getList()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToListIfUnboundElementsPresentShouldThrowException() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.list[0]", "foo-bar"); + source.put("foo.list[2]", "bar-baz"); + this.sources.add(source); + this.thrown.expect(BindException.class); + this.thrown.expectCause( + Matchers.instanceOf(UnboundConfigurationPropertiesException.class)); + this.binder.bind("foo", Bindable.of(ExampleListBean.class)); + } + + @Test + public void bindToClassShouldBindToSet() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.set[0]", "foo-bar"); + source.put("foo.set[1]", "bar-baz"); + this.sources.add(source); + ExampleSetBean bean = this.binder.bind("foo", Bindable.of(ExampleSetBean.class)) + .get(); + assertThat(bean.getSet()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToClassShouldBindToCollection() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.collection[0]", "foo-bar"); + source.put("foo.collection[1]", "bar-baz"); + this.sources.add(source); + ExampleCollectionBean bean = this.binder + .bind("foo", Bindable.of(ExampleCollectionBean.class)).get(); + assertThat(bean.getCollection()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToClassWhenHasNoSetterShouldBindToMap() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.map.foo-bar", "1"); + source.put("foo.map.bar-baz", "2"); + this.sources.add(source); + ExampleMapBeanWithoutSetter bean = this.binder + .bind("foo", Bindable.of(ExampleMapBeanWithoutSetter.class)).get(); + assertThat(bean.getMap()).containsExactly(entry(ExampleEnum.FOO_BAR, 1), + entry(ExampleEnum.BAR_BAZ, 2)); + } + + @Test + public void bindToClassWhenHasNoSetterShouldBindToList() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.list[0]", "foo-bar"); + source.put("foo.list[1]", "bar-baz"); + this.sources.add(source); + ExampleListBeanWithoutSetter bean = this.binder + .bind("foo", Bindable.of(ExampleListBeanWithoutSetter.class)).get(); + assertThat(bean.getList()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToClassWhenHasNoSetterShouldBindToSet() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.set[0]", "foo-bar"); + source.put("foo.set[1]", "bar-baz"); + this.sources.add(source); + ExampleSetBeanWithoutSetter bean = this.binder + .bind("foo", Bindable.of(ExampleSetBeanWithoutSetter.class)).get(); + assertThat(bean.getSet()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToClassWhenHasNoSetterShouldBindToCollection() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.collection[0]", "foo-bar"); + source.put("foo.collection[1]", "bar-baz"); + this.sources.add(source); + ExampleCollectionBeanWithoutSetter bean = this.binder + .bind("foo", Bindable.of(ExampleCollectionBeanWithoutSetter.class)).get(); + assertThat(bean.getCollection()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToClassShouldBindNested() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean.int-value", "123"); + source.put("foo.value-bean.string-value", "foo"); + this.sources.add(source); + ExampleNestedBean bean = this.binder + .bind("foo", Bindable.of(ExampleNestedBean.class)).get(); + assertThat(bean.getValueBean().getIntValue()).isEqualTo(123); + assertThat(bean.getValueBean().getStringValue()).isEqualTo("foo"); + } + + @Test + public void bindToClassWhenIterableShouldBindNestedBasedOnInstance() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean.int-value", "123"); + source.put("foo.value-bean.string-value", "foo"); + this.sources.add(source); + ExampleNestedBeanWithoutSetterOrType bean = this.binder + .bind("foo", Bindable.of(ExampleNestedBeanWithoutSetterOrType.class)) + .get(); + ExampleValueBean valueBean = (ExampleValueBean) bean.getValueBean(); + assertThat(valueBean.getIntValue()).isEqualTo(123); + assertThat(valueBean.getStringValue()).isEqualTo("foo"); + } + + @Test + public void bindToClassWhenNotIterableShouldNotBindNestedBasedOnInstance() + throws Exception { + // If we can't tell that binding will happen, we don't want to randomly invoke + // getters on the class and cause side effects + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean.int-value", "123"); + source.put("foo.value-bean.string-value", "foo"); + source.setNonIterable(true); + this.sources.add(source); + BindResult bean = this.binder.bind("foo", + Bindable.of(ExampleNestedBeanWithoutSetterOrType.class)); + assertThat(bean.isBound()).isFalse(); + } + + @Test + public void bindToClassWhenHasNoSetterShouldBindNested() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean.int-value", "123"); + source.put("foo.value-bean.string-value", "foo"); + this.sources.add(source); + ExampleNestedBeanWithoutSetter bean = this.binder + .bind("foo", Bindable.of(ExampleNestedBeanWithoutSetter.class)).get(); + assertThat(bean.getValueBean().getIntValue()).isEqualTo(123); + assertThat(bean.getValueBean().getStringValue()).isEqualTo("foo"); + } + + @Test + public void bindToClassWhenHasNoSetterAndImmutableShouldThrowException() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.nested.foo", "bar"); + this.sources.add(source); + this.thrown.expect(BindException.class); + this.binder.bind("foo", + Bindable.of(ExampleImmutableNestedBeanWithoutSetter.class)); + } + + @Test + public void bindToInstanceWhenNoNestedShouldLeaveNestedAsNull() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("faf.value-bean.int-value", "123"); + this.sources.add(source); + ExampleNestedBean bean = new ExampleNestedBean(); + BindResult boundBean = this.binder.bind("foo", + Bindable.of(ExampleNestedBean.class).withExistingValue(bean)); + assertThat(boundBean.isBound()).isFalse(); + assertThat(bean.getValueBean()).isNull(); + } + + @Test + public void bindToClassWhenPropertiesMissingShouldReturnUnbound() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("faf.int-value", "12"); + this.sources.add(source); + BindResult bean = this.binder.bind("foo", + Bindable.of(ExampleValueBean.class)); + assertThat(bean.isBound()).isFalse(); + } + + @Test + public void bindToClassWhenNoDefaultConstructorShouldReturnUnbound() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value", "bar"); + this.sources.add(source); + BindResult bean = this.binder.bind("foo", + Bindable.of(ExampleWithNonDefaultConstructor.class)); + assertThat(bean.isBound()).isFalse(); + } + + @Test + public void bindToInstanceWhenNoDefaultConstructorShouldBind() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value", "bar"); + this.sources.add(source); + ExampleWithNonDefaultConstructor bean = new ExampleWithNonDefaultConstructor( + "faf"); + ExampleWithNonDefaultConstructor boundBean = this.binder.bind("foo", Bindable + .of(ExampleWithNonDefaultConstructor.class).withExistingValue(bean)) + .get(); + assertThat(boundBean).isSameAs(bean); + assertThat(bean.getValue()).isEqualTo("bar"); + } + + @Test + public void bindToClassShouldBindHierarchy() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "123"); + source.put("foo.long-value", "456"); + this.sources.add(source); + ExampleSubclassBean bean = this.binder + .bind("foo", Bindable.of(ExampleSubclassBean.class)).get(); + assertThat(bean.getIntValue()).isEqualTo(123); + assertThat(bean.getLongValue()).isEqualTo(456); + } + + @Test + public void bindToClassWhenPropertyCannotBeConvertedShouldThrowException() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.int-value", "foo")); + this.thrown.expect(BindException.class); + this.binder.bind("foo", Bindable.of(ExampleValueBean.class)); + } + + @Test + public void bindToClassWhenPropertyCannotBeConvertedAndIgnoreErrorsShouldNotSetValue() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "12"); + source.put("foo.long-value", "bang"); + source.put("foo.string-value", "foo"); + source.put("foo.enum-value", "foo-bar"); + this.sources.add(source); + IgnoreErrorsBindHandler handler = new IgnoreErrorsBindHandler(); + ExampleValueBean bean = this.binder + .bind("foo", Bindable.of(ExampleValueBean.class), handler).get(); + assertThat(bean.getIntValue()).isEqualTo(12); + assertThat(bean.getLongValue()).isEqualTo(0); + assertThat(bean.getStringValue()).isEqualTo("foo"); + assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); + } + + @Test + public void bindToClassWhenMismatchedGetSetShouldBind() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value", "123"); + this.sources.add(source); + ExampleMismatchBean bean = this.binder + .bind("foo", Bindable.of(ExampleMismatchBean.class)).get(); + assertThat(bean.getValue()).isEqualTo("123"); + } + + @Test + public void bindToClassShouldNotInvokeExtraMethods() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource( + "foo.value", "123"); + source.setNonIterable(true); + this.sources.add(source); + ExampleWithThrowingGetters bean = this.binder + .bind("foo", Bindable.of(ExampleWithThrowingGetters.class)).get(); + assertThat(bean.getValue()).isEqualTo(123); + } + + @Test + public void bindToClassWithSelfReferenceShouldBind() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value", "123"); + this.sources.add(source); + ExampleWithSelfReference bean = this.binder + .bind("foo", Bindable.of(ExampleWithSelfReference.class)).get(); + assertThat(bean.getValue()).isEqualTo(123); + } + + @Test + public void bindtoInstanceWithExistingValueShouldReturnUnbound() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + this.sources.add(source); + ExampleNestedBean existingValue = new ExampleNestedBean(); + ExampleValueBean valueBean = new ExampleValueBean(); + existingValue.setValueBean(valueBean); + BindResult result = this.binder.bind("foo", + Bindable.of(ExampleNestedBean.class).withExistingValue(existingValue)); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindWithAnnotations() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.date", "2014-04-01"); + this.sources.add(source); + ConverterAnnotatedExampleBean bean = this.binder + .bind("foo", Bindable.of(ConverterAnnotatedExampleBean.class)).get(); + assertThat(bean.getDate().toString()).isEqualTo("2014-04-01"); + } + + public static class ExampleValueBean { + + private int intValue; + + private long longValue; + + private String stringValue; + + private ExampleEnum enumValue; + + public int getIntValue() { + return this.intValue; + } + + public void setIntValue(int intValue) { + this.intValue = intValue; + } + + public long getLongValue() { + return this.longValue; + } + + public void setLongValue(long longValue) { + this.longValue = longValue; + } + + public String getStringValue() { + return this.stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + public ExampleEnum getEnumValue() { + return this.enumValue; + } + + public void setEnumValue(ExampleEnum enumValue) { + this.enumValue = enumValue; + } + + } + + public static class ExampleDefaultsBean { + + private int foo = 123; + + private int bar = 456; + + public int getFoo() { + return this.foo; + } + + public void setFoo(int foo) { + this.foo = foo; + } + + public int getBar() { + return this.bar; + } + + public void setBar(int bar) { + this.bar = bar; + } + + } + + public static class ExampleMapBean { + + private Map map; + + public Map getMap() { + return this.map; + } + + public void setMap(Map map) { + this.map = map; + } + + } + + public static class ExampleListBean { + + private List list; + + public List getList() { + return this.list; + } + + public void setList(List list) { + this.list = list; + } + + } + + public static class ExampleSetBean { + + private Set set; + + public Set getSet() { + return this.set; + } + + public void setSet(Set set) { + this.set = set; + } + + } + + public static class ExampleCollectionBean { + + private Collection collection; + + public Collection getCollection() { + return this.collection; + } + + public void setCollection(Collection collection) { + this.collection = collection; + } + + } + + public static class ExampleMapBeanWithoutSetter { + + private Map map = new LinkedHashMap<>(); + + public Map getMap() { + return this.map; + } + + } + + public static class ExampleListBeanWithoutSetter { + + private List list = new ArrayList<>(); + + public List getList() { + return this.list; + } + + } + + public static class ExampleSetBeanWithoutSetter { + + private Set set = new LinkedHashSet<>(); + + public Set getSet() { + return this.set; + } + + } + + public static class ExampleCollectionBeanWithoutSetter { + + private Collection collection = new ArrayList<>(); + + public Collection getCollection() { + return this.collection; + } + + } + + public static class ExampleNestedBean { + + private ExampleValueBean valueBean; + + public ExampleValueBean getValueBean() { + return this.valueBean; + } + + public void setValueBean(ExampleValueBean valueBean) { + this.valueBean = valueBean; + } + + } + + public static class ExampleNestedBeanWithoutSetter { + + private ExampleValueBean valueBean = new ExampleValueBean(); + + public ExampleValueBean getValueBean() { + return this.valueBean; + } + + } + + public static class ExampleNestedBeanWithoutSetterOrType { + + private ExampleValueBean valueBean = new ExampleValueBean(); + + public Object getValueBean() { + return this.valueBean; + } + + } + + public static class ExampleImmutableNestedBeanWithoutSetter { + + private NestedImmutable nested = new NestedImmutable(); + + public NestedImmutable getNested() { + return this.nested; + } + + public static class NestedImmutable { + + public String getFoo() { + return "foo"; + } + + } + + } + + public static class ExampleWithNonDefaultConstructor { + + private String value; + + public ExampleWithNonDefaultConstructor(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + } + + public abstract static class ExampleSuperClassBean { + + private int intValue; + + public int getIntValue() { + return this.intValue; + } + + public void setIntValue(int intValue) { + this.intValue = intValue; + } + + } + + public static class ExampleSubclassBean extends ExampleSuperClassBean { + + private long longValue; + + public long getLongValue() { + return this.longValue; + } + + public void setLongValue(long longValue) { + this.longValue = longValue; + } + + } + + public static class ExampleMismatchBean { + + private int value; + + public String getValue() { + return String.valueOf(this.value); + } + + public void setValue(int value) { + this.value = value; + } + + } + + public static class ExampleWithThrowingGetters { + + private int value; + + public int getValue() { + return this.value; + } + + public void setValue(int value) { + this.value = value; + } + + public List getNames() { + throw new RuntimeException(); + } + + public ExampleValueBean getNested() { + throw new RuntimeException(); + } + + } + + public static class ExampleWithSelfReference { + + private int value; + + private ExampleWithSelfReference self; + + public int getValue() { + return this.value; + } + + public void setValue(int value) { + this.value = value; + } + + public ExampleWithSelfReference getSelf() { + return this.self; + } + + public void setSelf(ExampleWithSelfReference self) { + this.self = self; + } + + } + + public enum ExampleEnum { + + FOO_BAR, + + BAR_BAZ + + } + + public static class ConverterAnnotatedExampleBean { + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + private LocalDate date; + + public LocalDate getDate() { + return this.date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java new file mode 100644 index 000000000000..6776686110dc --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -0,0 +1,355 @@ +/* + * Copyright 2012-2017 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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.InOrder; + +import org.springframework.boot.context.properties.bind.BinderTests.ExampleEnum; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.test.context.support.TestPropertySourceUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link MapBinder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class MapBinderTests { + + private static final Bindable> STRING_STRING_MAP = Bindable + .mapOf(String.class, String.class); + + private static final Bindable> STRING_INTEGER_MAP = Bindable + .mapOf(String.class, Integer.class); + + private static final Bindable> INTEGER_INTEGER_MAP = Bindable + .mapOf(Integer.class, Integer.class); + + private static final Bindable> STRING_OBJECT_MAP = Bindable + .mapOf(String.class, Object.class); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void bindToMapShouldReturnPopulatedMap() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "1"); + source.put("foo.[baz]", "2"); + source.put("foo[BiNg]", "3"); + this.sources.add(source); + Map result = this.binder.bind("foo", STRING_STRING_MAP).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry("bar", "1"); + assertThat(result).containsEntry("baz", "2"); + assertThat(result).containsEntry("BiNg", "3"); + } + + @Test + @SuppressWarnings("unchecked") + public void bindToMapWithEmptyPrefix() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "1"); + this.sources.add(source); + Map result = this.binder.bind("", STRING_OBJECT_MAP).get(); + assertThat((Map) result.get("foo")).containsEntry("bar", "1"); + } + + @Test + public void bindToMapShouldConvertMapValue() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "1"); + source.put("foo.[baz]", "2"); + source.put("foo[BiNg]", "3"); + source.put("faf.bar", "x"); + this.sources.add(source); + Map result = this.binder.bind("foo", STRING_INTEGER_MAP).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry("bar", 1); + assertThat(result).containsEntry("baz", 2); + assertThat(result).containsEntry("BiNg", 3); + } + + @Test + public void bindToMapShouldBindToMapValue() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(Map.class, + ResolvableType.forClass(String.class), STRING_INTEGER_MAP.getType()); + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar.baz", "1"); + source.put("foo.bar.bin", "2"); + source.put("foo.far.baz", "3"); + source.put("foo.far.bin", "4"); + source.put("faf.far.bin", "x"); + this.sources.add(source); + Map> result = this.binder + .bind("foo", Bindable.>>of(type)).get(); + System.out.println(result); + assertThat(result).hasSize(2); + assertThat(result.get("bar")).containsEntry("baz", 1).containsEntry("bin", 2); + assertThat(result.get("far")).containsEntry("baz", 3).containsEntry("bin", 4); + } + + @Test + public void bindToMapShouldBindNestedMapValue() throws Exception { + ResolvableType nestedType = ResolvableType.forClassWithGenerics(Map.class, + ResolvableType.forClass(String.class), STRING_INTEGER_MAP.getType()); + ResolvableType type = ResolvableType.forClassWithGenerics(Map.class, + ResolvableType.forClass(String.class), nestedType); + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.nested.bar.baz", "1"); + source.put("foo.nested.bar.bin", "2"); + source.put("foo.nested.far.baz", "3"); + source.put("foo.nested.far.bin", "4"); + source.put("faf.nested.far.bin", "x"); + this.sources.add(source); + Bindable>>> target = Bindable + .of(type); + Map>> result = this.binder + .bind("foo", target).get(); + Map> nested = result.get("nested"); + assertThat(nested).hasSize(2); + assertThat(nested.get("bar")).containsEntry("baz", 1).containsEntry("bin", 2); + assertThat(nested.get("far")).containsEntry("baz", 3).containsEntry("bin", 4); + } + + @Test + @SuppressWarnings("unchecked") + public void bindToMapWhenMapValueIsObjectShouldBindNestedMapValue() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.nested.bar.baz", "1"); + source.put("foo.nested.bar.bin", "2"); + source.put("foo.nested.far.baz", "3"); + source.put("foo.nested.far.bin", "4"); + source.put("faf.nested.far.bin", "x"); + this.sources.add(source); + Map result = this.binder + .bind("foo", Bindable.mapOf(String.class, Object.class)).get(); + Map nested = (Map) result.get("nested"); + assertThat(nested).hasSize(2); + Map bar = (Map) nested.get("bar"); + assertThat(bar).containsEntry("baz", "1").containsEntry("bin", "2"); + Map far = (Map) nested.get("far"); + assertThat(far).containsEntry("baz", "3").containsEntry("bin", "4"); + } + + @Test + public void bindToMapWhenMapValueIsObjectAndNoRootShouldBindNestedMapValue() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("commit.id", "abcdefg"); + source.put("branch", "master"); + source.put("foo", "bar"); + this.sources.add(source); + Map result = this.binder + .bind("", Bindable.mapOf(String.class, Object.class)).get(); + assertThat(result.get("commit")) + .isEqualTo(Collections.singletonMap("id", "abcdefg")); + assertThat(result.get("branch")).isEqualTo("master"); + assertThat(result.get("foo")).isEqualTo("bar"); + } + + @Test + public void bindToMapWhenEmptyRootNameShouldBindMap() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("bar.baz", "1"); + source.put("bar.bin", "2"); + this.sources.add(source); + Map result = this.binder.bind("", STRING_INTEGER_MAP).get(); + assertThat(result).hasSize(2); + assertThat(result).containsEntry("bar.baz", 1).containsEntry("bar.bin", 2); + } + + @Test + public void bindToMapWhenMultipleCandidateShouldBindFirst() throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("foo.bar", "1"); + source1.put("foo.baz", "2"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo.baz", "3"); + source2.put("foo.bin", "4"); + this.sources.add(source2); + Map result = this.binder.bind("foo", STRING_INTEGER_MAP).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry("bar", 1); + assertThat(result).containsEntry("baz", 2); + assertThat(result).containsEntry("bin", 4); + } + + @Test + public void bindToMapWhenMultipleInSameSourceCandidateShouldBindFirst() + throws Exception { + Map map = new HashMap<>(); + map.put("foo.bar", "1"); + map.put("foo.b-az", "2"); + map.put("foo.ba-z", "3"); + map.put("foo.bin", "4"); + MapConfigurationPropertySource propertySource = new MapConfigurationPropertySource( + map); + this.sources.add(propertySource); + Map result = this.binder.bind("foo", STRING_INTEGER_MAP).get(); + assertThat(result).hasSize(4); + assertThat(result).containsEntry("bar", 1); + assertThat(result).containsEntry("b-az", 2); + assertThat(result).containsEntry("ba-z", 3); + assertThat(result).containsEntry("bin", 4); + } + + @Test + public void bindToMapWhenHasExistingMapShouldReplaceOnlyNewContents() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.bar", "1")); + Map existing = new HashMap<>(); + existing.put("bar", 1000); + existing.put("baz", 1001); + Bindable> target = STRING_INTEGER_MAP + .withExistingValue(existing); + Map result = this.binder.bind("foo", target).get(); + assertThat(result).isExactlyInstanceOf(HashMap.class); + assertThat(result).isSameAs(existing); + assertThat(result).hasSize(2); + assertThat(result).containsEntry("bar", 1); + assertThat(result).containsEntry("baz", 1001); + } + + @Test + public void bindToMapShouldRespectMapType() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.bar", "1")); + ResolvableType type = ResolvableType.forClassWithGenerics(HashMap.class, + String.class, Integer.class); + Object defaultMap = this.binder.bind("foo", STRING_INTEGER_MAP).get(); + Object customMap = this.binder.bind("foo", Bindable.of(type)).get(); + assertThat(customMap).isExactlyInstanceOf(HashMap.class) + .isNotInstanceOf(defaultMap.getClass()); + } + + @Test + public void bindToMapWhenNoValueShouldReturnUnbound() throws Exception { + this.sources.add(new MockConfigurationPropertySource("faf.bar", "1")); + BindResult> result = this.binder.bind("foo", + STRING_INTEGER_MAP); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToMapShouldConvertKey() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "1"); + source.put("foo[1]", "2"); + source.put("foo[9]", "3"); + this.sources.add(source); + Map result = this.binder.bind("foo", INTEGER_INTEGER_MAP).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry(0, 1); + assertThat(result).containsEntry(1, 2); + assertThat(result).containsEntry(9, 3); + } + + @Test + public void bindToMapShouldBeGreedyForStrings() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.aaa.bbb.ccc", "b"); + source.put("foo.bbb.ccc.ddd", "a"); + source.put("foo.ccc.ddd.eee", "r"); + this.sources.add(source); + Map result = this.binder.bind("foo", STRING_STRING_MAP).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry("aaa.bbb.ccc", "b"); + assertThat(result).containsEntry("bbb.ccc.ddd", "a"); + assertThat(result).containsEntry("ccc.ddd.eee", "r"); + } + + @Test + public void bindToMapShouldBeGreedyForScalars() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.aaa.bbb.ccc", "foo-bar"); + source.put("foo.bbb.ccc.ddd", "BAR_BAZ"); + source.put("foo.ccc.ddd.eee", "bazboo"); + this.sources.add(source); + Map result = this.binder + .bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry("aaa.bbb.ccc", ExampleEnum.FOO_BAR); + assertThat(result).containsEntry("bbb.ccc.ddd", ExampleEnum.BAR_BAZ); + assertThat(result).containsEntry("ccc.ddd.eee", ExampleEnum.BAZ_BOO); + } + + @Test + public void bindToMapWithPlaceholdersShouldBeGreedyForScalars() throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, "foo=boo"); + MockConfigurationPropertySource source = new MockConfigurationPropertySource( + "foo.aaa.bbb.ccc", "baz-${foo}"); + this.sources.add(source); + this.binder = new Binder(this.sources, + new PropertySourcesPlaceholdersResolver(environment)); + Map result = this.binder + .bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)).get(); + assertThat(result).containsEntry("aaa.bbb.ccc", ExampleEnum.BAZ_BOO); + } + + @Test + public void bindToMapWithNoPropertiesShouldReturnUnbound() throws Exception { + this.binder = new Binder(this.sources); + BindResult> result = this.binder.bind("foo", + Bindable.mapOf(String.class, ExampleEnum.class)); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToMapShouldTriggerOnSuccess() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.bar", "1", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + Bindable> target = STRING_INTEGER_MAP; + this.binder.bind("foo", target, handler); + InOrder inOrder = inOrder(handler); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo.bar")), + eq(Bindable.of(Integer.class)), any(), eq(1)); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(target), any(), isA(Map.class)); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolverTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolverTests.java new file mode 100644 index 000000000000..2bcbf40e5e2d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolverTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2017 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; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySources; +import org.springframework.util.PropertyPlaceholderHelper; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertySourcesPlaceholdersResolver}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class PropertySourcesPlaceholdersResolverTests { + + private PropertySourcesPlaceholdersResolver resolver; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void placeholderResolverIfEnvironmentNullShouldThrowException() + throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Environment must not be null"); + new PropertySourcesPlaceholdersResolver((Environment) null); + } + + @Test + public void resolveIfPlaceholderPresentResolvesProperty() { + MutablePropertySources sources = getPropertySources(); + this.resolver = new PropertySourcesPlaceholdersResolver(sources); + Object resolved = this.resolver.resolvePlaceholders("${FOO}"); + assertThat(resolved).isEqualTo("hello world"); + } + + @Test + public void resolveIfPlaceholderAbsentUsesDefault() { + this.resolver = new PropertySourcesPlaceholdersResolver((PropertySources) null); + Object resolved = this.resolver.resolvePlaceholders("${FOO:bar}"); + assertThat(resolved).isEqualTo("bar"); + } + + @Test + public void resolveIfPlaceholderAbsentAndNoDefaultShouldThrowException() { + this.resolver = new PropertySourcesPlaceholdersResolver((PropertySources) null); + this.thrown.expect(IllegalArgumentException.class); + this.thrown + .expectMessage("Could not resolve placeholder 'FOO' in value \"${FOO}\""); + this.resolver.resolvePlaceholders("${FOO}"); + } + + @Test + public void resolveIfHelperPresentShouldUseIt() { + MutablePropertySources sources = getPropertySources(); + TestPropertyPlaceholderHelper helper = new TestPropertyPlaceholderHelper("$<", + ">"); + this.resolver = new PropertySourcesPlaceholdersResolver(sources, helper); + Object resolved = this.resolver.resolvePlaceholders("$"); + assertThat(resolved).isEqualTo("hello world"); + } + + private MutablePropertySources getPropertySources() { + MutablePropertySources sources = new MutablePropertySources(); + Map source = new HashMap<>(); + source.put("FOO", "hello world"); + sources.addFirst(new MapPropertySource("test", source)); + return sources; + } + + static class TestPropertyPlaceholderHelper extends PropertyPlaceholderHelper { + + TestPropertyPlaceholderHelper(String placeholderPrefix, + String placeholderSuffix) { + super(placeholderPrefix, placeholderSuffix); + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java new file mode 100644 index 000000000000..517834f82375 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2017 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.net.InetAddress; +import java.net.UnknownHostException; + +import org.junit.AssumptionViolatedException; + +/** + * Base class for {@link InetAddress} tests. + * + * @author Phillip Webb + */ +public abstract class AbstractInetAddressTests { + + public void assumeResolves(String host) { + try { + InetAddress.getByName(host); + } + catch (UnknownHostException ex) { + throw new AssumptionViolatedException("Host " + host + " not resolvable", ex); + } + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java new file mode 100644 index 000000000000..6674c6879ef4 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012-2017 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.io.InputStream; +import java.net.InetAddress; + +import org.junit.Before; +import org.junit.Test; + +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link BinderConversionService}. + * + * @author Phillip Webb + */ +public class BinderConversionServiceTests { + + private ConversionService delegate; + + private BinderConversionService service; + + @Before + public void setup() { + this.delegate = mock(ConversionService.class); + this.service = new BinderConversionService(this.delegate); + } + + @Test + public void createConversionServiceShouldAcceptNullConversionService() + throws Exception { + BinderConversionService service = new BinderConversionService(null); + assertThat(service.canConvert(String.class, TestEnum.class)).isTrue(); + assertThat(service.canConvert(TypeDescriptor.valueOf(String.class), + TypeDescriptor.valueOf(TestEnum.class))).isTrue(); + assertThat(service.convert("ONE", TestEnum.class)).isEqualTo(TestEnum.ONE); + assertThat(service.convert("ONE", TypeDescriptor.valueOf(String.class), + TypeDescriptor.valueOf(TestEnum.class))).isEqualTo(TestEnum.ONE); + } + + @Test + public void canConvertShouldDelegateToConversionService() throws Exception { + Class from = String.class; + Class to = InputStream.class; + given(this.delegate.canConvert(from, to)).willReturn(true); + assertThat(this.service.canConvert(from, to)).isEqualTo(true); + verify(this.delegate).canConvert(from, to); + } + + @Test + public void canConvertTypeDescriptorShouldDelegateToConversionService() + throws Exception { + TypeDescriptor from = TypeDescriptor.valueOf(String.class); + TypeDescriptor to = TypeDescriptor.valueOf(InputStream.class); + given(this.delegate.canConvert(from, to)).willReturn(true); + assertThat(this.service.canConvert(from, to)).isEqualTo(true); + verify(this.delegate).canConvert(from, to); + } + + @Test + public void convertShouldDelegateToConversionService() throws Exception { + String from = "foo"; + InputStream to = mock(InputStream.class); + given(this.delegate.convert(from, InputStream.class)).willReturn(to); + assertThat(this.service.convert(from, InputStream.class)).isEqualTo(to); + verify(this.delegate).convert(from, InputStream.class); + } + + @Test + public void convertTargetTypeShouldDelegateToConversionService() throws Exception { + String from = "foo"; + InputStream to = mock(InputStream.class); + TypeDescriptor fromType = TypeDescriptor.valueOf(String.class); + TypeDescriptor toType = TypeDescriptor.valueOf(InputStream.class); + given(this.delegate.convert(from, fromType, toType)).willReturn(to); + assertThat(this.service.convert(from, fromType, toType)).isEqualTo(to); + verify(this.delegate).convert(from, fromType, toType); + } + + @Test + public void convertShouldSwallowDelegateConversionFailedException() throws Exception { + given(this.delegate.convert("one", TestEnum.class)) + .willThrow(new ConversionFailedException(null, null, null, null)); + assertThat(this.service.convert("one", TestEnum.class)).isEqualTo(TestEnum.ONE); + verify(this.delegate).convert("one", TestEnum.class); + } + + @Test + public void conversionServiceShouldSupportEnums() throws Exception { + this.service = new BinderConversionService(null); + assertThat(this.service.canConvert(String.class, TestEnum.class)).isTrue(); + assertThat(this.service.convert("one", TestEnum.class)).isEqualTo(TestEnum.ONE); + assertThat(this.service.convert("t-w-o", TestEnum.class)).isEqualTo(TestEnum.TWO); + } + + @Test + public void conversionServiceShouldSupportStringToCharArray() throws Exception { + this.service = new BinderConversionService(null); + assertThat(this.service.canConvert(String.class, char[].class)).isTrue(); + assertThat(this.service.convert("test", char[].class)).containsExactly('t', 'e', + 's', 't'); + } + + @Test + public void conversionServiceShouldSupportStringToInetAddress() throws Exception { + this.service = new BinderConversionService(null); + assertThat(this.service.canConvert(String.class, InetAddress.class)).isTrue(); + } + + @Test + public void conversionServiceShouldSupportInetAddressToString() throws Exception { + this.service = new BinderConversionService(null); + assertThat(this.service.canConvert(InetAddress.class, String.class)).isTrue(); + } + + @Test + public void conversionServiceShouldSupportStringToResource() throws Exception { + this.service = new BinderConversionService(null); + Resource resource = this.service.convert( + "org/springframework/boot/context/properties/bind/convert/resource.txt", + Resource.class); + assertThat(resource).isNotNull(); + } + + @Test + public void conversionServiceShouldSupportStringToClass() throws Exception { + this.service = new BinderConversionService(null); + Class converted = this.service.convert(InputStream.class.getName(), + Class.class); + assertThat(converted).isEqualTo(InputStream.class); + } + + enum TestEnum { + + ONE, TWO + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java new file mode 100644 index 000000000000..4e3402254a87 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2017 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.net.InetAddress; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link InetAddressToStringConverter}. + * + * @author Phillip Webb + */ +public class InetAddressToStringConverterTests extends AbstractInetAddressTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private InetAddressToStringConverter converter = new InetAddressToStringConverter(); + + @Test + public void convertShouldConvertToHostAddress() throws Exception { + assumeResolves("example.com"); + InetAddress address = InetAddress.getByName("example.com"); + String converted = this.converter.convert(address); + assertThat(converted).isEqualTo(address.getHostAddress()); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java new file mode 100644 index 000000000000..0e3b6fcf8e04 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2017 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.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; + +import org.junit.Test; + +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertyEditorConverter}. + * + * @author Phillip Webb + */ +public class PropertyEditorConverterTests { + + private final PropertyEditorConverter converter = new PropertyEditorConverter(); + + @Test + public void matchesShouldLimitToPropertyEditor() { + String converted = new SimpleTypeConverter().convertIfNecessary(123, + String.class); + assertThat(converted).isEqualTo("123"); + // Even though the SimpleTypeConverter can convert, we should limit to just + // PropertyEditors not implicit support + assertThat(this.converter.matches(TypeDescriptor.valueOf(Integer.class), + TypeDescriptor.valueOf(String.class))).isFalse(); + } + + @Test + public void convertShouldSupportConventionBasedEditors() throws Exception { + String source = "org/springframework/boot/context/properties/bind/convert/resource.txt"; + TypeDescriptor sourceType = TypeDescriptor.forObject(source); + TypeDescriptor targetType = TypeDescriptor.valueOf(Resource.class); + assertThat(this.converter.matches(sourceType, targetType)).isTrue(); + Object converted = this.converter.convert(source, sourceType, targetType); + assertThat(converted).isNotNull().isInstanceOf(Resource.class); + assertThat(converted.toString()).endsWith("resource.txt]"); + } + + @Test + public void convertShouldSupportDefaultEditors() throws Exception { + String source = "en_UK"; + TypeDescriptor sourceType = TypeDescriptor.forObject(source); + TypeDescriptor targetType = TypeDescriptor.valueOf(Locale.class); + assertThat(this.converter.matches(sourceType, targetType)).isTrue(); + Object converted = this.converter.convert(source, sourceType, targetType); + assertThat(converted).isNotNull().isInstanceOf(Locale.class); + assertThat(converted.toString()).endsWith("en_UK"); + } + + @Test + public void matchShouldNotMatchCollection() throws Exception { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + assertThat(this.converter.matches(sourceType, + TypeDescriptor.valueOf(Collection.class))).isFalse(); + assertThat(this.converter.matches(sourceType, TypeDescriptor.valueOf(List.class))) + .isFalse(); + assertThat(this.converter.matches(sourceType, TypeDescriptor.valueOf(Set.class))) + .isFalse(); + } + + @Test + public void matchShouldNotMatchMap() throws Exception { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + assertThat(this.converter.matches(sourceType, TypeDescriptor.valueOf(Map.class))) + .isFalse(); + assertThat(this.converter.matches(sourceType, + TypeDescriptor.valueOf(SortedMap.class))).isFalse(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptorTests.java new file mode 100644 index 000000000000..12e0a8616e1a --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptorTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2017 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.Annotation; +import java.util.List; + +import org.junit.Test; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.TypeDescriptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ResolvableTypeDescriptor}. + * + * @author Phillip Webb + */ +public class ResolvableTypeDescriptorTests { + + @Test + public void forBindableShouldIncludeType() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(List.class, + String.class); + Bindable bindable = Bindable.of(type); + TypeDescriptor descriptor = ResolvableTypeDescriptor.forBindable(bindable); + assertThat(descriptor.getResolvableType()).isEqualTo(type); + } + + @Test + public void forBindableShouldIncludeAnnotations() throws Exception { + Annotation annotation = AnnotationUtils.synthesizeAnnotation(Test.class); + Bindable bindable = Bindable.of(String.class).withAnnotations(annotation); + TypeDescriptor descriptor = ResolvableTypeDescriptor.forBindable(bindable); + assertThat(descriptor.getAnnotations()).containsExactly(annotation); + } + + @Test + public void forTypeShouldIncludeType() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(List.class, + String.class); + TypeDescriptor descriptor = ResolvableTypeDescriptor.forType(type); + assertThat(descriptor.getResolvableType()).isEqualTo(type); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java new file mode 100644 index 000000000000..a880d56e0def --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2017 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 org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StringToCharArrayConverter}. + * + * @author Phillip Webb + */ +public class StringToCharArrayConverterTests { + + private StringToCharArrayConverter converter = new StringToCharArrayConverter(); + + @Test + public void convertShouldConvertSource() throws Exception { + char[] converted = this.converter.convert("test"); + assertThat(converted).containsExactly('t', 'e', 's', 't'); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java new file mode 100644 index 000000000000..f7274b1062e4 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2017 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 org.junit.Test; + +import org.springframework.core.convert.converter.Converter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StringToEnumConverterFactory}. + * + * @author Phillip Webb + */ +public class StringToEnumConverterFactoryTests { + + private StringToEnumConverterFactory factory = new StringToEnumConverterFactory(); + + @Test + public void getConverterShouldReturnConverter() { + Converter converter = this.factory.getConverter(TestEnum.class); + assertThat(converter).isNotNull(); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void getConverterWhenEnumSubclassShouldReturnConverter() throws Exception { + Converter converter = this.factory + .getConverter((Class) TestSubclassEnum.ONE.getClass()); + assertThat(converter).isNotNull(); + } + + @Test + public void convertWhenExactMatchShouldConvertValue() throws Exception { + Converter converter = this.factory.getConverter(TestEnum.class); + assertThat(converter.convert("")).isNull(); + assertThat(converter.convert("ONE")).isEqualTo(TestEnum.ONE); + assertThat(converter.convert("TWO")).isEqualTo(TestEnum.TWO); + assertThat(converter.convert("THREE_AND_FOUR")) + .isEqualTo(TestEnum.THREE_AND_FOUR); + } + + @Test + public void convertWhenFuzzyMatchShouldConvertValue() throws Exception { + Converter converter = this.factory.getConverter(TestEnum.class); + assertThat(converter.convert("")).isNull(); + assertThat(converter.convert("one")).isEqualTo(TestEnum.ONE); + assertThat(converter.convert("tWo")).isEqualTo(TestEnum.TWO); + assertThat(converter.convert("three_and_four")) + .isEqualTo(TestEnum.THREE_AND_FOUR); + assertThat(converter.convert("threeandfour")).isEqualTo(TestEnum.THREE_AND_FOUR); + assertThat(converter.convert("three-and-four")) + .isEqualTo(TestEnum.THREE_AND_FOUR); + assertThat(converter.convert("threeAndFour")).isEqualTo(TestEnum.THREE_AND_FOUR); + } + + enum TestEnum { + + ONE, TWO, THREE_AND_FOUR + + } + + enum TestSubclassEnum { + + ONE { + + @Override + public String toString() { + return "foo"; + } + + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java new file mode 100644 index 000000000000..ca15cfed5dfa --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2017 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.net.InetAddress; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StringToInetAddressConverter}. + * + * @author Phillip Webb + */ +public class StringToInetAddressConverterTests extends AbstractInetAddressTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private StringToInetAddressConverter converter = new StringToInetAddressConverter(); + + @Test + public void convertWhenHostDoesNotExistShouldThrowException() { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Unknown host"); + this.converter.convert("ireallydontexist.example.com"); + } + + @Test + public void convertWhenHostExistsShouldConvert() throws Exception { + assumeResolves("example.com"); + InetAddress converted = this.converter.convert("example.com"); + assertThat(converted.toString()).startsWith("example.com"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandlerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandlerTests.java new file mode 100644 index 000000000000..efc1870f4035 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandlerTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2017 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.handler; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IgnoreErrorsBindHandler}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class IgnoreErrorsBindHandlerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + } + + @Test + public void bindWhenNotIgnoringErrorsShouldFail() throws Exception { + this.thrown.expect(BindException.class); + this.binder.bind("example", Bindable.of(Example.class)); + } + + @Test + public void bindWhenIgnoringErrorsShouldBind() throws Exception { + Example bound = this.binder.bind("example", Bindable.of(Example.class), + new IgnoreErrorsBindHandler()).get(); + assertThat(bound.getFoo()).isEqualTo(0); + } + + public static class Example { + + private int foo; + + public int getFoo() { + return this.foo; + } + + public void setFoo(int foo) { + this.foo = foo; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandlerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandlerTests.java new file mode 100644 index 000000000000..f58d5c76fa00 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandlerTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2017 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.handler; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IgnoreNestedPropertiesBindHandler}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class IgnoreNestedPropertiesBindHandlerTests { + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "foovalue"); + source.put("example.nested.bar", "barvalue"); + this.sources.add(source); + this.binder = new Binder(this.sources); + } + + @Test + public void bindWhenNotIngoringNestedShouldBindAll() throws Exception { + Example bound = this.binder.bind("example", Bindable.of(Example.class)).get(); + assertThat(bound.getFoo()).isEqualTo("foovalue"); + assertThat(bound.getNested().getBar()).isEqualTo("barvalue"); + } + + @Test + public void bindWhenIngoringNestedShouldFilterNested() throws Exception { + Example bound = this.binder.bind("example", Bindable.of(Example.class), + new IgnoreNestedPropertiesBindHandler()).get(); + assertThat(bound.getFoo()).isEqualTo("foovalue"); + assertThat(bound.getNested()).isNull(); + } + + public static class Example { + + private String foo; + + private ExampleNested nested; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public ExampleNested getNested() { + return this.nested; + } + + public void setNested(ExampleNested nested) { + this.nested = nested; + } + + } + + public static class ExampleNested { + + private String bar; + + public String getBar() { + return this.bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java new file mode 100644 index 000000000000..0733005839da --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2017 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.handler; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link NoUnboundElementsBindHandler}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class NoUnboundElementsBindHandlerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Test + public void bindWhenNotUsingNoUnboundElementsHandlerShouldBind() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "bar"); + source.put("example.baz", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + Example bound = this.binder + .bind(ConfigurationPropertyName.of("example"), Bindable.of(Example.class)) + .get(); + assertThat(bound.getFoo()).isEqualTo("bar"); + } + + @Test + public void bindWhenUsingNoUnboundElementsHandlerShouldBind() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + Example bound = this.binder.bind("example", Bindable.of(Example.class), + new NoUnboundElementsBindHandler()).get(); + assertThat(bound.getFoo()).isEqualTo("bar"); + } + + @Test + public void bindWhenUsingNoUnboundElementsHandlerThrowException() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "bar"); + source.put("example.baz", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + try { + this.binder.bind("example", Bindable.of(Example.class), + new NoUnboundElementsBindHandler()); + fail("did not throw"); + } + catch (BindException ex) { + assertThat(ex.getCause().getMessage()) + .contains("The elements [example.baz] were left unbound"); + } + } + + @Test + public void bindWhenUsingNoUnboundElementsHandlerShouldBindIfPrefixDifferent() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "bar"); + source.put("other.baz", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + Example bound = this.binder.bind("example", Bindable.of(Example.class), + new NoUnboundElementsBindHandler()).get(); + assertThat(bound.getFoo()).isEqualTo("bar"); + } + + public static class Example { + + private String foo; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/test/PackagePrivateBeanBindingTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/test/PackagePrivateBeanBindingTests.java new file mode 100644 index 000000000000..70a4c830e00b --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/test/PackagePrivateBeanBindingTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2017 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.test; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Binder} using package private Java beans. + * + * @author Madhura Bhave + */ +public class PackagePrivateBeanBindingTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private Binder binder; + + private ConfigurationPropertyName name; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + this.name = ConfigurationPropertyName.of("foo"); + } + + @Test + public void bindToPackagePrivateClassShouldBindToInstance() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "999"); + this.sources.add(source); + ExamplePackagePrivateBean bean = this.binder + .bind(this.name, Bindable.of(ExamplePackagePrivateBean.class)).get(); + assertThat(bean.getBar()).isEqualTo(999); + } + + static class ExamplePackagePrivateBean { + + private int bar; + + public int getBar() { + return this.bar; + } + + public void setBar(int bar) { + this.bar = bar; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/BindValidationExceptionTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/BindValidationExceptionTests.java new file mode 100644 index 000000000000..083cc67a09a5 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/BindValidationExceptionTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2017 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.validation; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BindValidationException}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class BindValidationExceptionTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenValidationErrorsIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ValidationErrors must not be null"); + new BindValidationException(null); + } + + @Test + public void getValidationErrorsShouldReturnValidationErrors() throws Exception { + ValidationErrors errors = mock(ValidationErrors.class); + BindValidationException exception = new BindValidationException(errors); + assertThat(exception.getValidationErrors()).isEqualTo(errors); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldErrorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldErrorTests.java new file mode 100644 index 000000000000..53980fa2e611 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldErrorTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2017 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.validation; + +import org.junit.Test; + +import org.springframework.boot.origin.MockOrigin; +import org.springframework.boot.origin.Origin; +import org.springframework.validation.FieldError; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OriginTrackedFieldError}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class OriginTrackedFieldErrorTests { + + private static final FieldError FIELD_ERROR = new FieldError("foo", "bar", "faf"); + + private static final Origin ORIGIN = MockOrigin.of("afile"); + + @Test + public void ofWhenFieldErrorIsNullShouldReturnNull() throws Exception { + assertThat(OriginTrackedFieldError.of(null, ORIGIN)).isNull(); + } + + @Test + public void ofWhenOriginIsNullShouldReturnFieldErrorWithoutOrigin() throws Exception { + assertThat(OriginTrackedFieldError.of(FIELD_ERROR, null)).isSameAs(FIELD_ERROR); + } + + @Test + public void ofShouldReturnOriginCapableFieldError() throws Exception { + FieldError fieldError = OriginTrackedFieldError.of(FIELD_ERROR, ORIGIN); + assertThat(fieldError.getObjectName()).isEqualTo("foo"); + assertThat(fieldError.getField()).isEqualTo("bar"); + assertThat(Origin.from(fieldError)).isEqualTo(ORIGIN); + } + + @Test + public void toStringShouldAddOrigin() throws Exception { + assertThat(OriginTrackedFieldError.of(FIELD_ERROR, ORIGIN).toString()).isEqualTo( + "Field error in object 'foo' on field 'bar': rejected value [null]" + + "; codes []; arguments []; default message [faf]; origin afile"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java new file mode 100644 index 000000000000..2b02fdefce8d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java @@ -0,0 +1,249 @@ +/* + * Copyright 2012-2017 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.validation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.boot.origin.Origin; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.validation.annotation.Validated; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.instanceOf; + +/** + * Tests for {@link ValidationBindHandler}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ValidationBindHandlerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private ValidationBindHandler handler; + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + this.handler = new ValidationBindHandler(validator); + } + + @Test + public void bindShouldBindWithoutHandler() { + this.sources.add(new MockConfigurationPropertySource("foo.age", 4)); + ExampleValidatedBean bean = this.binder + .bind("foo", Bindable.of(ExampleValidatedBean.class)).get(); + assertThat(bean.getAge()).isEqualTo(4); + } + + @Test + public void bindShouldFailWithHandler() { + this.sources.add(new MockConfigurationPropertySource("foo.age", 4)); + this.thrown.expect(BindException.class); + this.thrown.expectCause(instanceOf(BindValidationException.class)); + this.binder.bind("foo", Bindable.of(ExampleValidatedBean.class), this.handler); + } + + @Test + public void bindShouldValidateNestedProperties() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.nested.age", 4)); + this.thrown.expect(BindException.class); + this.thrown.expectCause(instanceOf(BindValidationException.class)); + this.binder.bind("foo", Bindable.of(ExampleValidatedWithNestedBean.class), + this.handler); + } + + @Test + public void bindShouldFailWithAccessToOrigin() { + this.sources.add(new MockConfigurationPropertySource("foo.age", 4, "file")); + BindValidationException cause = bindAndExpectValidationError( + () -> this.binder.bind(ConfigurationPropertyName.of("foo"), + Bindable.of(ExampleValidatedBean.class), this.handler)); + ObjectError objectError = cause.getValidationErrors().getAllErrors().get(0); + assertThat(Origin.from(objectError).toString()).isEqualTo("file"); + } + + @Test + public void bindShouldFailWithAccessToBoundProperties() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.nested.name", "baz"); + source.put("foo.nested.age", "4"); + source.put("faf.bar", "baz"); + this.sources.add(source); + BindValidationException cause = bindAndExpectValidationError( + () -> this.binder.bind(ConfigurationPropertyName.of("foo"), + Bindable.of(ExampleValidatedWithNestedBean.class), this.handler)); + Set boundProperties = cause.getValidationErrors() + .getBoundProperties(); + assertThat(boundProperties).extracting((p) -> p.getName().toString()) + .containsExactly("foo.nested.age", "foo.nested.name"); + } + + @Test + public void bindShouldFailWithAccessToName() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.nested.age", "4")); + BindValidationException cause = bindAndExpectValidationError( + () -> this.binder.bind(ConfigurationPropertyName.of("foo"), + Bindable.of(ExampleValidatedWithNestedBean.class), this.handler)); + assertThat(cause.getValidationErrors().getName().toString()) + .isEqualTo("foo.nested"); + } + + @Test + public void bindShouldFailIfExistingValueIsInvalid() throws Exception { + ExampleValidatedBean existingValue = new ExampleValidatedBean(); + BindValidationException cause = bindAndExpectValidationError( + () -> this.binder.bind(ConfigurationPropertyName.of("foo"), Bindable + .of(ExampleValidatedBean.class).withExistingValue(existingValue), + this.handler)); + FieldError fieldError = (FieldError) cause.getValidationErrors().getAllErrors() + .get(0); + assertThat(fieldError.getField()).isEqualTo("age"); + } + + @Test + public void bindShouldNotValidateWithoutAnnotation() throws Exception { + ExampleNonValidatedBean existingValue = new ExampleNonValidatedBean(); + this.binder.bind(ConfigurationPropertyName.of("foo"), Bindable + .of(ExampleNonValidatedBean.class).withExistingValue(existingValue), + this.handler); + } + + private BindValidationException bindAndExpectValidationError(Runnable action) { + try { + action.run(); + } + catch (BindException ex) { + ex.printStackTrace(); + + BindValidationException cause = (BindValidationException) ex.getCause(); + return cause; + } + throw new IllegalStateException("Did not throw"); + } + + public static class ExampleNonValidatedBean { + + @Min(5) + private int age; + + public int getAge() { + return this.age; + } + + public void setAge(int age) { + this.age = age; + } + + } + + @Validated + public static class ExampleValidatedBean { + + @Min(5) + private int age; + + public int getAge() { + return this.age; + } + + public void setAge(int age) { + this.age = age; + } + + } + + @Validated + public static class ExampleValidatedWithNestedBean { + + @Valid + private ExampleNested nested = new ExampleNested(); + + public ExampleNested getNested() { + return this.nested; + } + + public void setNested(ExampleNested nested) { + this.nested = nested; + } + + } + + public static class ExampleNested { + + private String name; + + @Min(5) + private int age; + + @NotNull + private String address; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return this.age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getAddress() { + return this.address; + } + + public void setAddress(String address) { + this.address = address; + } + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationErrorsTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationErrorsTests.java new file mode 100644 index 000000000000..b1c829aeb999 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationErrorsTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2017 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.validation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.origin.MockOrigin; +import org.springframework.boot.origin.Origin; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ValidationErrors}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ValidationErrorsTests { + + private static final ConfigurationPropertyName NAME = ConfigurationPropertyName + .of("foo"); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenNameIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be null"); + new ValidationErrors(null, Collections.emptySet(), Collections.emptyList()); + } + + @Test + public void createWhenBoundPropertiesIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("BoundProperties must not be null"); + new ValidationErrors(NAME, null, Collections.emptyList()); + } + + @Test + public void createWhenErrorsIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Errors must not be null"); + new ValidationErrors(NAME, Collections.emptySet(), null); + } + + @Test + public void getNameShouldReturnName() throws Exception { + ConfigurationPropertyName name = NAME; + ValidationErrors errors = new ValidationErrors(name, Collections.emptySet(), + Collections.emptyList()); + assertThat((Object) errors.getName()).isEqualTo(name); + } + + @Test + public void getBoundPropertiesShouldReturnBoundProperties() throws Exception { + Set boundProperties = new LinkedHashSet<>(); + boundProperties.add(new ConfigurationProperty(NAME, "foo", null)); + ValidationErrors errors = new ValidationErrors(NAME, boundProperties, + Collections.emptyList()); + assertThat(errors.getBoundProperties()).isEqualTo(boundProperties); + } + + @Test + public void getErrorsShouldReturnErrors() throws Exception { + List allErrors = new ArrayList<>(); + allErrors.add(new ObjectError("foo", "bar")); + ValidationErrors errors = new ValidationErrors(NAME, Collections.emptySet(), + allErrors); + assertThat(errors.getAllErrors()).isEqualTo(allErrors); + } + + @Test + public void iteratorShouldIterateErrors() throws Exception { + List allErrors = new ArrayList<>(); + allErrors.add(new ObjectError("foo", "bar")); + ValidationErrors errors = new ValidationErrors(NAME, Collections.emptySet(), + allErrors); + assertThat(errors.iterator()).containsExactlyElementsOf(allErrors); + } + + @Test + public void getErrorsShouldAdaptFieldErrorsToBeOriginProviders() throws Exception { + Set boundProperties = new LinkedHashSet<>(); + ConfigurationPropertyName name1 = ConfigurationPropertyName.of("foo.bar"); + Origin origin1 = MockOrigin.of("line1"); + boundProperties.add(new ConfigurationProperty(name1, "boot", origin1)); + ConfigurationPropertyName name2 = ConfigurationPropertyName.of("foo.baz.bar"); + Origin origin2 = MockOrigin.of("line2"); + boundProperties.add(new ConfigurationProperty(name2, "boot", origin2)); + List allErrors = new ArrayList<>(); + allErrors.add(new FieldError("objectname", "bar", "message")); + ValidationErrors errors = new ValidationErrors( + ConfigurationPropertyName.of("foo.baz"), boundProperties, allErrors); + assertThat(Origin.from(errors.getAllErrors().get(0))).isEqualTo(origin2); + } + +} diff --git a/spring-boot/src/test/resources/org/springframework/boot/context/properties/bind/convert/resource.txt b/spring-boot/src/test/resources/org/springframework/boot/context/properties/bind/convert/resource.txt new file mode 100644 index 000000000000..9daeafb9864c --- /dev/null +++ b/spring-boot/src/test/resources/org/springframework/boot/context/properties/bind/convert/resource.txt @@ -0,0 +1 @@ +test