Skip to content

Commit

Permalink
Add support for configuration properties binding
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb committed Apr 27, 2017
1 parent a5651b7 commit 813128a
Show file tree
Hide file tree
Showing 64 changed files with 7,498 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}

}
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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);

}
Loading

0 comments on commit 813128a

Please sign in to comment.