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
This commit is contained in:
Phillip Webb
2017-04-17 11:43:09 -07:00
parent a5651b75de
commit 813128a410
64 changed files with 7498 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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 <T> the type being bound
* @author Phillip Webb
* @author Madhura Bhave
*/
abstract class AggregateBinder<T> {
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 <T> The aggregate type
*/
protected static class AggregateSupplier<T> {
private final Supplier<T> supplier;
private T supplied;
public AggregateSupplier(Supplier<T> 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;
}
}
}

View File

@@ -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);
}

View File

@@ -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<Object> {
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<Object> list = (List<Object>) 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;
}
}

View File

@@ -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;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
/**
* 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 target the binable to bind
* @param hasKnownBindableProperties if this binder has known bindable elements. If
* names from underlying {@link ConfigurationPropertySource} cannot be iterated this
* method can be {@code false}, even though binding may ultimately succeed.
* @param propertyBinder property binder
* @param <T> The source type
* @return a bound instance or {@code null}
*/
<T> T bind(Bindable<T> target, boolean hasKnownBindableProperties,
BeanPropertyBinder propertyBinder);
}

View File

@@ -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;
/**
* Binder that can be used by {@link BeanBinder} implementations to recursively bind bean
* properties.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
interface BeanPropertyBinder {
/**
* 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);
}

View File

@@ -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;
/**
* Internal utility to help when dealing with Java Bean property names.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
abstract class BeanPropertyName {
private BeanPropertyName() {
}
/**
* 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) {
return toDashedForm(name, 0);
}
/**
* Return the specified Java Bean property name in dashed form.
* @param name the source name
* @param start the starting char
* @return the dashed from
*/
public static String toDashedForm(String name, int start) {
StringBuilder result = new StringBuilder();
char[] chars = name.replace("_", "-").toCharArray();
for (int i = start; i < chars.length; i++) {
char ch = chars[i];
if (Character.isUpperCase(ch) && result.length() > 0
&& result.charAt(result.length() - 1) != '-') {
result.append("-");
}
result.append(Character.toLowerCase(ch));
}
return result.toString();
}
}

View File

@@ -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<ConfigurationPropertySource> 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();
}

View File

@@ -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();
}
}

View File

@@ -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 {
}
}

View File

@@ -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 <T> The result type
* @author Phillip Webb
* @author Madhura Bhave
* @since 2.0.0
*/
public final class BindResult<T> {
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<? super T> 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 <U> 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 <U> BindResult<U> map(Function<? super T, ? extends U> 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<? extends T> 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<? extends T> 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 <X> 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 <X extends Throwable> T orElseThrow(Supplier<? extends X> 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 <T> BindResult<T> of(T value) {
if (value == null) {
return (BindResult<T>) UNBOUND;
}
return new BindResult<T>(value);
}
}

View File

@@ -0,0 +1,235 @@
/*
* 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 <T> 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<T> {
private static final Annotation[] NO_ANNOTATIONS = {};
private final ResolvableType type;
private final ResolvableType boxedType;
private final Supplier<T> value;
private final Annotation[] annotations;
private Bindable(ResolvableType type, ResolvableType boxedType, Supplier<T> 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<T> 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<T> withAnnotations(Annotation... annotations) {
return new Bindable<T>(this.type, this.boxedType, this.value,
(annotations == null ? NO_ANNOTATIONS : annotations));
}
public Bindable<T> withExistingValue(T existingValue) {
Assert.isTrue(
existingValue == null || this.type.isArray()
|| this.boxedType.resolve().isInstance(existingValue),
"ExistingValue must be an instance of " + this.type);
Supplier<T> value = (existingValue == null ? null : () -> existingValue);
return new Bindable<>(this.type, this.boxedType, value, NO_ANNOTATIONS);
}
public Bindable<T> withSuppliedValue(Supplier<T> 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 <T> 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 <T> Bindable<T> ofInstance(T instance) {
Assert.notNull(instance, "Instance must not be null");
Class<T> type = (Class<T>) instance.getClass();
return of(type).withExistingValue(instance);
}
/**
* Create a new {@link Bindable} of the specified type.
* @param <T> The source type
* @param type the type (must not be {@code null})
* @return a {@link Bindable} instance
* @see #of(ResolvableType)
*/
public static <T> Bindable<T> of(Class<T> 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 <E> the element type
* @param elementType the list element type
* @return a {@link Bindable} instance
*/
public static <E> Bindable<List<E>> listOf(Class<E> elementType) {
return of(ResolvableType.forClassWithGenerics(List.class, elementType));
}
/**
* Create a new {@link Bindable} {@link Set} of the specified element type.
* @param <E> the element type
* @param elementType the set element type
* @return a {@link Bindable} instance
*/
public static <E> Bindable<Set<E>> setOf(Class<E> elementType) {
return of(ResolvableType.forClassWithGenerics(Set.class, elementType));
}
/**
* Create a new {@link Bindable} {@link Map} of the specified kay and value type.
* @param <K> the key type
* @param <V> the value type
* @param keyType the map key type
* @param valueType the map value type
* @return a {@link Bindable} instance
*/
public static <K, V> Bindable<Map<K, V>> mapOf(Class<K> keyType, Class<V> valueType) {
return of(ResolvableType.forClassWithGenerics(Map.class, keyType, valueType));
}
/**
* Create a new {@link Bindable} of the specified type.
* @param <T> The source type
* @param type the type (must not be {@code null})
* @return a {@link Bindable} instance
* @see #of(Class)
*/
public static <T> Bindable<T> 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;
}
}

View File

@@ -0,0 +1,435 @@
/*
* 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.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
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<Class<?>> NON_BEAN_CLASSES = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(Object.class, Class.class)));
private static final List<BeanBinder> BEAN_BINDERS;
static {
List<BeanBinder> beanBinders = new ArrayList<>();
beanBinders.add(new JavaBeanBinder());
BEAN_BINDERS = Collections.unmodifiableList(beanBinders);
}
private final Iterable<ConfigurationPropertySource> 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<ConfigurationPropertySource> 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<ConfigurationPropertySource> 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<ConfigurationPropertySource> 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 = (conversionService instanceof BinderConversionService
? (BinderConversionService) 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 <T> the bound type
* @return the binding result (never {@code null})
* @see #bind(ConfigurationPropertyName, Bindable, BindHandler)
*/
public <T> BindResult<T> bind(String name, Class<T> 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 <T> the bound type
* @return the binding result (never {@code null})
* @see #bind(ConfigurationPropertyName, Bindable, BindHandler)
*/
public <T> BindResult<T> bind(String name, Bindable<T> 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 <T> the bound type
* @return the binding result (never {@code null})
* @see #bind(ConfigurationPropertyName, Bindable, BindHandler)
*/
public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> 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 <T> the bound type
* @return the binding result (never {@code null})
*/
public <T> BindResult<T> bind(String name, Bindable<T> 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 <T> the bound type
* @return the binding result (never {@code null})
*/
public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> 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> T bind(ConfigurationPropertyName name, Bindable<T> 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> T handleBindResult(ConfigurationPropertyName name, Bindable<T> 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> T handleBindError(ConfigurationPropertyName name, Bindable<T> 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> T convert(Object value, Bindable<T> target) {
if (value == null) {
return null;
}
return this.conversionService.convert(value, target);
}
private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> 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, context);
if (property != null) {
return bindProperty(name, target, handler, context, property);
}
return bindBean(name, target, handler, context);
}
private AggregateBinder<?> getAggregateBinder(Bindable<?> target, Context context) {
Class<?> resolvedType = target.getType().resolve();
if (Map.class.isAssignableFrom(resolvedType)) {
return new MapBinder(context);
}
if (Collection.class.isAssignableFrom(resolvedType)) {
return new CollectionBinder(context);
}
if (target.getType().isArray()) {
return new ArrayBinder(context);
}
return null;
}
private <T> Object bindAggregate(ConfigurationPropertyName name, Bindable<T> target,
BindHandler handler, Context context, AggregateBinder<?> aggregateBinder) {
AggregateElementBinder elementBinder = (itemName, itemTarget, source) -> {
return context.withSource(source,
() -> Binder.this.bind(itemName, itemTarget, handler, context));
};
return context.withIncreasedDepth(
() -> aggregateBinder.bind(name, target, elementBinder));
}
private ConfigurationProperty findProperty(ConfigurationPropertyName name,
Context context) {
return context.streamSources()
.map((source) -> source.getConfigurationProperty(name))
.filter(Objects::nonNull).findFirst().orElse(null);
}
private <T> Object bindProperty(ConfigurationPropertyName name, Bindable<T> 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 {
boolean hasKnownBindableProperties = context.streamSources()
.flatMap((s) -> s.filter(name::isAncestorOf).stream()).findAny()
.isPresent();
if (!hasKnownBindableProperties && isUnbindableBean(target)) {
return null;
}
BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(
name.append(propertyName), propertyTarget, handler, context);
Class<?> type = target.getType().resolve();
if (context.hasBoundBean(type)) {
return null;
}
return context.withBean(type, () -> {
Stream<?> boundBeans = BEAN_BINDERS.stream().map(
(b) -> b.bind(target, hasKnownBindableProperties, propertyBinder));
return boundBeans.filter(Objects::nonNull).findFirst().orElse(null);
});
}
private boolean isUnbindableBean(Bindable<?> target) {
Class<?> resolved = target.getType().resolve();
if (resolved.isPrimitive() || NON_BEAN_CLASSES.contains(resolved)) {
return true;
}
String packageName = ClassUtils.getPackageName(resolved);
return packageName.startsWith("java.");
}
/**
* 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 int depth;
private int sourcePushCount;
private final List<ConfigurationPropertySource> source = Arrays
.asList((ConfigurationPropertySource) null);
private final Deque<Class<?>> beans = new ArrayDeque<>();
private ConfigurationProperty configurationProperty;
void increaseDepth() {
this.depth++;
}
void decreaseDepth() {
this.depth--;
}
@Override
public int getDepth() {
return this.depth;
}
public <T> T withSource(ConfigurationPropertySource source,
Supplier<T> supplier) {
if (source == null) {
return supplier.get();
}
this.source.set(0, source);
this.sourcePushCount++;
try {
return supplier.get();
}
finally {
this.sourcePushCount--;
}
}
public <T> T withBean(Class<?> bean, Supplier<T> supplier) {
this.beans.push(bean);
try {
return withIncreasedDepth(supplier);
}
finally {
this.beans.pop();
}
}
public <T> T withIncreasedDepth(Supplier<T> supplier) {
increaseDepth();
try {
return supplier.get();
}
finally {
decreaseDepth();
}
}
private Stream<ConfigurationPropertySource> streamSources() {
if (this.sourcePushCount > 0) {
return this.source.stream();
}
return StreamSupport.stream(Binder.this.sources.spliterator(), false);
}
@Override
public Iterable<ConfigurationPropertySource> getSources() {
if (this.sourcePushCount > 0) {
return this.source;
}
return Binder.this.sources;
}
public boolean hasBoundBean(Class<?> bean) {
return this.beans.contains(bean);
}
@Override
public ConfigurationProperty getConfigurationProperty() {
return this.configurationProperty;
}
void setConfigurationProperty(ConfigurationProperty configurationProperty) {
this.configurationProperty = configurationProperty;
}
@Override
public PlaceholdersResolver getPlaceholdersResolver() {
return Binder.this.placeholdersResolver;
}
@Override
public BinderConversionService getConversionService() {
return Binder.this.conversionService;
}
}
}

View File

@@ -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<Collection<Object>> {
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<Object> merge(Collection<Object> existing,
Collection<Object> additional) {
existing.clear();
existing.addAll(additional);
return existing;
}
}

View File

@@ -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 <T> the type being bound
* @author Phillip Webb
* @author Madhura Bhave
*/
abstract class IndexedElementsBinder<T> extends AggregateBinder<T> {
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<Object> 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<String, ConfigurationProperty> knownIndexedChildren = getKnownIndexedChildren(
source, root);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
ConfigurationPropertyName name = root.appendIndex(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<String, ConfigurationProperty> getKnownIndexedChildren(
ConfigurationPropertySource source, ConfigurationPropertyName root) {
MultiValueMap<String, ConfigurationProperty> 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<String, ConfigurationProperty> children) {
if (!children.isEmpty()) {
throw new UnboundConfigurationPropertiesException(
children.values().stream().flatMap(List::stream)
.collect(Collectors.toCollection(TreeSet::new)));
}
}
@SuppressWarnings("unchecked")
private <C> 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<Collection<Object>> {
public IndexedCollectionSupplier(Supplier<Collection<Object>> supplier) {
super(supplier);
}
}
}

View File

@@ -0,0 +1,309 @@
/*
* 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.Introspector;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.beans.BeanUtils;
import org.springframework.core.ResolvableType;
/**
* {@link BeanBinder} for mutable Java Beans.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class JavaBeanBinder implements BeanBinder {
@Override
public <T> T bind(Bindable<T> target, boolean hasKnownBindableProperties,
BeanPropertyBinder propertyBinder) {
Bean<T> bean = Bean.get(target, hasKnownBindableProperties);
if (bean == null) {
return null;
}
BeanSupplier<T> beanSupplier = bean.getSupplier(target);
boolean bound = bind(target, propertyBinder, bean, beanSupplier);
return (bound ? beanSupplier.get() : null);
}
private <T> boolean bind(Bindable<T> target, BeanPropertyBinder propertyBinder,
Bean<T> bean, BeanSupplier<T> beanSupplier) {
boolean bound = false;
for (Map.Entry<String, BeanProperty> entry : bean.getProperties().entrySet()) {
bound |= bind(beanSupplier, propertyBinder, entry.getValue());
}
return bound;
}
private <T> boolean bind(BeanSupplier<T> beanSupplier,
BeanPropertyBinder propertyBinder, BeanProperty property) {
String propertyName = property.getName();
ResolvableType type = property.getType();
Supplier<Object> value = property.getValue(beanSupplier);
Annotation[] annotations = property.getAnnotations();
Object bound = propertyBinder.bindProperty(propertyName,
Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations));
if (bound == null) {
return false;
}
if (property.isSettable()) {
property.setValue(beanSupplier, bound);
}
else if (value == null || !bound.equals(value.get())) {
throw new IllegalStateException(
"No setter found for property: " + property.getName());
}
return true;
}
/**
* The bean being bound.
*/
private static class Bean<T> {
private static Bean<?> cached;
private final Class<?> type;
private final Map<String, BeanProperty> properties = new LinkedHashMap<>();
Bean(Class<?> type) {
this.type = type;
putProperties(type);
}
private void putProperties(Class<?> type) {
while (type != null && !Object.class.equals(type)) {
for (Method method : type.getDeclaredMethods()) {
if (isCandidate(method)) {
addMethod(method);
}
}
for (Field field : type.getDeclaredFields()) {
addField(field);
}
type = type.getSuperclass();
}
}
private boolean isCandidate(Method method) {
return Modifier.isPublic(method.getModifiers())
&& !Object.class.equals(method.getDeclaringClass())
&& !Class.class.equals(method.getDeclaringClass());
}
private void addMethod(Method method) {
String name = method.getName();
int parameterCount = method.getParameterCount();
if (name.startsWith("get") && parameterCount == 0) {
name = Introspector.decapitalize(name.substring(3));
this.properties.computeIfAbsent(name, BeanProperty::new)
.addGetter(method);
}
else if (name.startsWith("is") && parameterCount == 0) {
name = Introspector.decapitalize(name.substring(2));
this.properties.computeIfAbsent(name, BeanProperty::new)
.addGetter(method);
}
else if (name.startsWith("set") && parameterCount == 1) {
name = Introspector.decapitalize(name.substring(3));
this.properties.computeIfAbsent(name, BeanProperty::new)
.addSetter(method);
}
}
private void addField(Field field) {
BeanProperty property = this.properties.get(field.getName());
if (property != null) {
property.addField(field);
}
}
public Class<?> getType() {
return this.type;
}
public Map<String, BeanProperty> getProperties() {
return this.properties;
}
@SuppressWarnings("unchecked")
public BeanSupplier<T> getSupplier(Bindable<T> target) {
return new BeanSupplier<T>(() -> {
T instance = null;
if (target.getValue() != null) {
instance = target.getValue().get();
}
if (instance == null) {
instance = (T) BeanUtils.instantiateClass(this.type);
}
return instance;
});
}
@SuppressWarnings("unchecked")
public static <T> Bean<T> get(Bindable<T> bindable,
boolean useExistingValueForType) {
Class<?> type = bindable.getType().resolve();
Supplier<T> value = bindable.getValue();
if (value == null && !isInstantiatable(type)) {
return null;
}
if (useExistingValueForType && value != null) {
T instance = value.get();
type = (instance != null ? instance.getClass() : type);
}
Bean<?> bean = Bean.cached;
if (bean == null || !type.equals(bean.getType())) {
bean = new Bean<>(type);
cached = bean;
}
return (Bean<T>) bean;
}
private static boolean isInstantiatable(Class<?> type) {
if (type.isInterface()) {
return false;
}
try {
type.getDeclaredConstructor();
return true;
}
catch (Exception ex) {
return false;
}
}
}
private static class BeanSupplier<T> implements Supplier<T> {
private final Supplier<T> factory;
private T instance;
BeanSupplier(Supplier<T> factory) {
this.factory = factory;
}
@Override
public T get() {
if (this.instance == null) {
this.instance = this.factory.get();
}
return this.instance;
}
}
/**
* A bean property being bound.
*/
private static class BeanProperty {
private final String name;
private Method getter;
private Method setter;
private Field field;
BeanProperty(String name) {
this.name = BeanPropertyName.toDashedForm(name);
}
public void addGetter(Method getter) {
if (this.getter == null) {
this.getter = getter;
}
}
public void addSetter(Method setter) {
if (this.setter == null) {
this.setter = setter;
}
}
public void addField(Field field) {
if (this.field == null) {
this.field = field;
}
}
public String getName() {
return this.name;
}
public ResolvableType getType() {
if (this.setter != null) {
return ResolvableType.forMethodParameter(this.setter, 0);
}
return ResolvableType.forMethodReturnType(this.getter);
}
public Annotation[] getAnnotations() {
try {
return (this.field == null ? null : this.field.getDeclaredAnnotations());
}
catch (Exception ex) {
return null;
}
}
public Supplier<Object> getValue(Supplier<?> instance) {
if (this.getter == null) {
return null;
}
return () -> {
try {
this.getter.setAccessible(true);
return this.getter.invoke(instance.get());
}
catch (Exception ex) {
throw new IllegalStateException(
"Unable to get value for property " + this.name, ex);
}
};
}
public boolean isSettable() {
return this.setter != null;
}
public void setValue(Supplier<?> instance, Object value) {
try {
this.setter.setAccessible(true);
this.setter.invoke(instance.get(), value);
}
catch (Exception ex) {
throw new IllegalStateException(
"Unable to set value for property " + this.name, ex);
}
}
}
}

View File

@@ -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<Map<Object, Object>> {
MapBinder(BindContext context) {
super(context);
}
@Override
protected Object bind(ConfigurationPropertyName name, Bindable<?> target,
AggregateElementBinder elementBinder, Class<?> type) {
Map<Object, Object> 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<Object, Object> merge(Map<Object, Object> existing,
Map<Object, Object> 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<Object, Object> 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("."));
}
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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<ConfigurationProperty> unboundProperties;
public UnboundConfigurationPropertiesException(
Set<ConfigurationProperty> unboundProperties) {
super(buildMessage(unboundProperties));
this.unboundProperties = Collections.unmodifiableSet(unboundProperties);
}
public Set<ConfigurationProperty> getUnboundProperties() {
return this.unboundProperties;
}
private static String buildMessage(Set<ConfigurationProperty> 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();
}
}

View File

@@ -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> 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> T convert(Object value, Bindable<T> bindable) {
TypeDescriptor sourceType = TypeDescriptor.forObject(value);
TypeDescriptor targetType = ResolvableTypeDescriptor.forBindable(bindable);
return (T) convert(value, sourceType, targetType);
}
@Override
public <T> T convert(Object source, Class<T> targetType) {
return callConversionService((c) -> c.convert(source, targetType));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
return callConversionService((c) -> c.convert(source, sourceType, targetType));
}
private <T> T callConversionService(Function<ConversionService, T> call) {
if (this.conversionService == null) {
return callAdditionalConversionService(call, null);
}
try {
return call.apply(this.conversionService);
}
catch (ConversionException ex) {
return callAdditionalConversionService(call, ex);
}
}
private <T> T callAdditionalConversionService(Function<ConversionService, T> call,
RuntimeException cause) {
try {
return call.apply(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;
}
}

View File

@@ -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<InetAddress, String> {
@Override
public String convert(InetAddress source) {
return source.getHostAddress();
}
}

View File

@@ -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<Class<?>> SKIPPED;
static {
Set<Class<?>> 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<ConvertiblePair> 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());
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, char[]> {
@Override
public char[] convert(String source) {
return source.toCharArray();
}
}

View File

@@ -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<String, Enum> {
@Override
public <T extends Enum> Converter<String, T> getConverter(Class<T> 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<T extends Enum> implements Converter<String, T> {
private final Class<T> enumType;
StringToEnum(Class<T> 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<T>) 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();
}
}
}

View File

@@ -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<String, InetAddress> {
@Override
public InetAddress convert(String source) {
try {
return InetAddress.getByName(source);
}
catch (UnknownHostException ex) {
throw new IllegalStateException("Unknown host " + source, ex);
}
}
}

View File

@@ -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;

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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<ConfigurationPropertyName> 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<ConfigurationProperty> 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);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<ConfigurationProperty> 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<ConfigurationProperty> 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);
}
}

View File

@@ -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<ObjectError> {
private final ConfigurationPropertyName name;
private final Set<ConfigurationProperty> boundProperties;
private final List<ObjectError> errors;
ValidationErrors(ConfigurationPropertyName name,
Set<ConfigurationProperty> boundProperties, List<ObjectError> 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<ObjectError> convertErrors(ConfigurationPropertyName name,
Set<ConfigurationProperty> boundProperties, List<ObjectError> errors) {
List<ObjectError> 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<ConfigurationProperty> boundProperties, ObjectError error) {
if (error instanceof FieldError) {
return convertFieldError(name, boundProperties, (FieldError) error);
}
return error;
}
private FieldError convertFieldError(ConfigurationPropertyName name,
Set<ConfigurationProperty> boundProperties, FieldError error) {
if (error instanceof ObjectProvider<?>) {
return error;
}
return OriginTrackedFieldError.of(error,
findFieldErrorOrigin(name, boundProperties, error));
}
private Origin findFieldErrorOrigin(ConfigurationPropertyName name,
Set<ConfigurationProperty> 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<ConfigurationProperty> getBoundProperties() {
return this.boundProperties;
}
public boolean hasErrors() {
return !this.errors.isEmpty();
}
/**
* Return the list of all validation errors.
* @return the errors
*/
public List<ObjectError> getAllErrors() {
return this.errors;
}
@Override
public Iterator<ObjectError> iterator() {
return this.errors.iterator();
}
}

View File

@@ -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;

View File

@@ -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<List<Integer>> INTEGER_LIST = Bindable
.listOf(Integer.class);
private static final Bindable<Integer[]> INTEGER_ARRAY = Bindable.of(Integer[].class);
private List<ConfigurationPropertySource> 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<Integer[][]> 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<List<Integer>[]> target = Bindable.of(type);
List<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 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<ConfigurationProperty> 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<Integer[]> 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<Integer[]> 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();
}
}

View File

@@ -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");
}
}

View File

@@ -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<String> consumer;
@Mock
private Function<String, String> mapper;
@Mock
private Supplier<String> supplier;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
@Test
public void getWhenHasValueShouldReturnValue() throws Exception {
BindResult<String> result = BindResult.of("foo");
assertThat(result.get()).isEqualTo("foo");
}
@Test
public void getWhenHasNoValueShouldThrowException() throws Exception {
BindResult<String> result = BindResult.of(null);
this.thrown.expect(NoSuchElementException.class);
this.thrown.expectMessage("No value bound");
result.get();
}
@Test
public void isBoundWhenHasValueShouldReturnTrue() throws Exception {
BindResult<String> result = BindResult.of("foo");
assertThat(result.isBound()).isTrue();
}
@Test
public void isBoundWhenHasNoValueShouldFalse() throws Exception {
BindResult<String> result = BindResult.of(null);
assertThat(result.isBound()).isFalse();
}
@Test
public void ifBoundWhenConsumerIsNullShouldThrowException() throws Exception {
BindResult<String> 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<String> result = BindResult.of("foo");
result.ifBound(this.consumer);
verify(this.consumer).accept("foo");
}
@Test
public void ifBoundWhenHasNoValueShouldNotCallConsumer() throws Exception {
BindResult<String> result = BindResult.of(null);
result.ifBound(this.consumer);
verifyZeroInteractions(this.consumer);
}
@Test
public void mapWhenMapperIsNullShouldThrowException() throws Exception {
BindResult<String> 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<String> 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<String> result = BindResult.of(null);
result.map(this.mapper);
verifyZeroInteractions(this.mapper);
}
@Test
public void orElseWhenHasValueShouldReturnValue() throws Exception {
BindResult<String> result = BindResult.of("foo");
assertThat(result.orElse("bar")).isEqualTo("foo");
}
@Test
public void orElseWhenHasValueNoShouldReturnOther() throws Exception {
BindResult<String> result = BindResult.of(null);
assertThat(result.orElse("bar")).isEqualTo("bar");
}
@Test
public void orElseGetWhenHasValueShouldReturnValue() throws Exception {
BindResult<String> result = BindResult.of("foo");
assertThat(result.orElseGet(this.supplier)).isEqualTo("foo");
verifyZeroInteractions(this.supplier);
}
@Test
public void orElseGetWhenHasValueNoShouldReturnOther() throws Exception {
BindResult<String> result = BindResult.of(null);
given(this.supplier.get()).willReturn("bar");
assertThat(result.orElseGet(this.supplier)).isEqualTo("bar");
}
@Test
public void orElseCreateWhenTypeIsNullShouldThrowException() throws Exception {
BindResult<String> 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<ExampleBean> result = BindResult.of(new ExampleBean("foo"));
assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("foo");
}
@Test
public void orElseCreateWhenHasValueNoShouldReturnCreatedValue() throws Exception {
BindResult<ExampleBean> result = BindResult.of(null);
assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("new");
}
@Test
public void orElseThrowWhenHasValueShouldReturnValue() throws Exception {
BindResult<String> result = BindResult.of("foo");
assertThat(result.orElseThrow(IOException::new)).isEqualTo("foo");
}
@Test
public void orElseThrowWhenHasNoValueShouldThrowException() throws Exception {
BindResult<String> 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<Object> result = BindResult.of("foo");
assertThat(result.isBound()).isTrue();
assertThat(result.get()).isEqualTo("foo");
}
@Test
public void ofWhenValueIsNullShouldReturnUnbound() throws Exception {
BindResult<Object> 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;
}
}
}

View File

@@ -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<Integer> bindable = Bindable
.<Integer>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<String> bindable = Bindable.of(String.class);
assertThat(bindable.getBoxedType())
.isEqualTo(ResolvableType.forClass(String.class));
}
@Test
public void getBoxedTypeWhenPrimativeShouldReturnBoxedType() throws Exception {
Bindable<Integer> 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<int[]> 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<String> 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<Annotation>["
+ "@org.springframework.boot.context.properties.bind."
+ "BindableTests$TestAnnotation()]");
}
@Test
public void equalsAndHashcode() throws Exception {
Annotation annotation = AnnotationUtils
.synthesizeAnnotation(TestAnnotation.class);
Bindable<String> bindable1 = Bindable.of(String.class).withExistingValue("foo")
.withAnnotations(annotation);
Bindable<String> bindable2 = Bindable.of(String.class).withExistingValue("foo")
.withAnnotations(annotation);
Bindable<String> 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;
}
}
}

View File

@@ -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<ConfigurationPropertySource> 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<ConfigurationPropertySource>) 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<String> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<JavaBean> 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
}
}

View File

@@ -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<List<Integer>> INTEGER_LIST = Bindable
.listOf(Integer.class);
private static final Bindable<List<String>> STRING_LIST = Bindable
.listOf(String.class);
private List<ConfigurationPropertySource> 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<Integer> 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<List<List<Integer>>> target = Bindable.of(
ResolvableType.forClassWithGenerics(List.class, INTEGER_LIST.getType()));
List<List<Integer>> 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<Integer> 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<ConfigurationProperty> 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<Integer> 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<Integer> 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<Integer> existing = new LinkedList<>();
existing.add(1000);
existing.add(1001);
List<Integer> 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<Integer> existing = new LinkedList<>();
existing.add(1000);
BindResult<List<Integer>> 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<List<Integer>> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<String> 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<String> result = this.binder.bind("foo", STRING_LIST).get();
assertThat(result).isNotNull().isEmpty();
}
}

View File

@@ -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<ConfigurationPropertySource> 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<ExampleDefaultsBean> 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<ExampleNestedBeanWithoutSetterOrType> 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<ExampleNestedBean> 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<ExampleValueBean> 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<ExampleWithNonDefaultConstructor> 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<ExampleNestedBean> 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<ExampleEnum, Integer> map;
public Map<ExampleEnum, Integer> getMap() {
return this.map;
}
public void setMap(Map<ExampleEnum, Integer> map) {
this.map = map;
}
}
public static class ExampleListBean {
private List<ExampleEnum> list;
public List<ExampleEnum> getList() {
return this.list;
}
public void setList(List<ExampleEnum> list) {
this.list = list;
}
}
public static class ExampleSetBean {
private Set<ExampleEnum> set;
public Set<ExampleEnum> getSet() {
return this.set;
}
public void setSet(Set<ExampleEnum> set) {
this.set = set;
}
}
public static class ExampleCollectionBean {
private Collection<ExampleEnum> collection;
public Collection<ExampleEnum> getCollection() {
return this.collection;
}
public void setCollection(Collection<ExampleEnum> collection) {
this.collection = collection;
}
}
public static class ExampleMapBeanWithoutSetter {
private Map<ExampleEnum, Integer> map = new LinkedHashMap<>();
public Map<ExampleEnum, Integer> getMap() {
return this.map;
}
}
public static class ExampleListBeanWithoutSetter {
private List<ExampleEnum> list = new ArrayList<>();
public List<ExampleEnum> getList() {
return this.list;
}
}
public static class ExampleSetBeanWithoutSetter {
private Set<ExampleEnum> set = new LinkedHashSet<>();
public Set<ExampleEnum> getSet() {
return this.set;
}
}
public static class ExampleCollectionBeanWithoutSetter {
private Collection<ExampleEnum> collection = new ArrayList<>();
public Collection<ExampleEnum> 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<String> 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;
}
}
}

View File

@@ -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<Map<String, String>> STRING_STRING_MAP = Bindable
.mapOf(String.class, String.class);
private static final Bindable<Map<String, Integer>> STRING_INTEGER_MAP = Bindable
.mapOf(String.class, Integer.class);
private static final Bindable<Map<Integer, Integer>> INTEGER_INTEGER_MAP = Bindable
.mapOf(Integer.class, Integer.class);
private static final Bindable<Map<String, Object>> STRING_OBJECT_MAP = Bindable
.mapOf(String.class, Object.class);
private List<ConfigurationPropertySource> 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<String, String> 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<String, Object> result = this.binder.bind("", STRING_OBJECT_MAP).get();
assertThat((Map<String, Object>) 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<String, Integer> 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<String, Map<String, Integer>> result = this.binder
.bind("foo", Bindable.<Map<String, Map<String, Integer>>>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<Map<String, Map<String, Map<String, Integer>>>> target = Bindable
.of(type);
Map<String, Map<String, Map<String, Integer>>> result = this.binder
.bind("foo", target).get();
Map<String, Map<String, Integer>> 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<String, Object> result = this.binder
.bind("foo", Bindable.mapOf(String.class, Object.class)).get();
Map<String, Object> nested = (Map<String, Object>) result.get("nested");
assertThat(nested).hasSize(2);
Map<String, Object> bar = (Map<String, Object>) nested.get("bar");
assertThat(bar).containsEntry("baz", "1").containsEntry("bin", "2");
Map<String, Object> far = (Map<String, Object>) 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<String, Object> 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<String, Integer> 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<String, Integer> 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<String, Object> 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<String, Integer> 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<String, Integer> existing = new HashMap<>();
existing.put("bar", 1000);
existing.put("baz", 1001);
Bindable<Map<String, Integer>> target = STRING_INTEGER_MAP
.withExistingValue(existing);
Map<String, Integer> 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<Map<String, Integer>> 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<Integer, Integer> 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<String, String> 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<String, ExampleEnum> 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<String, ExampleEnum> 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<Map<String, ExampleEnum>> 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<Map<String, Integer>> 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));
}
}

View File

@@ -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("$<FOO>");
assertThat(resolved).isEqualTo("hello world");
}
private MutablePropertySources getPropertySources() {
MutablePropertySources sources = new MutablePropertySources();
Map<String, Object> 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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<String> from = String.class;
Class<InputStream> 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
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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');
}
}

View File

@@ -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<String, TestEnum> converter = this.factory.getConverter(TestEnum.class);
assertThat(converter).isNotNull();
}
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
public void getConverterWhenEnumSubclassShouldReturnConverter() throws Exception {
Converter<String, TestEnum> converter = this.factory
.getConverter((Class) TestSubclassEnum.ONE.getClass());
assertThat(converter).isNotNull();
}
@Test
public void convertWhenExactMatchShouldConvertValue() throws Exception {
Converter<String, TestEnum> 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<String, TestEnum> 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";
}
}
}
}

View File

@@ -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");
}
}

View File

@@ -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<ConfigurationPropertySource> 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;
}
}
}

View File

@@ -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<ConfigurationPropertySource> 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;
}
}
}

View File

@@ -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<ConfigurationPropertySource> 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;
}
}
}

View File

@@ -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<ConfigurationPropertySource> 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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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<ConfigurationPropertySource> 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<ConfigurationProperty> boundProperties = cause.getValidationErrors()
.getBoundProperties();
assertThat(boundProperties).extracting((p) -> p.getName().toString())
.contains("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;
}
}
}

View File

@@ -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<ConfigurationProperty> 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<ObjectError> 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<ObjectError> 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<ConfigurationProperty> 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<ObjectError> 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);
}
}