diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java index 3d7427f3b6..aaab2f084c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java @@ -76,12 +76,12 @@ public class ProjectInfoAutoConfiguration { protected Properties loadFrom(Resource location, String prefix, Charset encoding) throws IOException { - String p = prefix.endsWith(".") ? prefix : prefix + "."; + prefix = prefix.endsWith(".") ? prefix : prefix + "."; Properties source = loadSource(location, encoding); Properties target = new Properties(); for (String key : source.stringPropertyNames()) { - if (key.startsWith(p)) { - target.put(key.substring(p.length()), source.get(key)); + if (key.startsWith(prefix)) { + target.put(key.substring(prefix.length()), source.get(key)); } } return target; @@ -93,9 +93,7 @@ public class ProjectInfoAutoConfiguration { return PropertiesLoaderUtils .loadProperties(new EncodedResource(location, encoding)); } - else { - return PropertiesLoaderUtils.loadProperties(location); - } + return PropertiesLoaderUtils.loadProperties(location); } static class GitResourceAvailableCondition extends SpringBootCondition { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindHandlerAdvisor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindHandlerAdvisor.java new file mode 100644 index 0000000000..a125df5b30 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindHandlerAdvisor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindHandler; + +/** + * Allows additional functionality to be applied to the {@link BindHandler} used by the + * {@link ConfigurationPropertiesBindingPostProcessor}. + * + * @author Phillip Webb + * @since 2.1.0 + * @see AbstractBindHandler + */ +@FunctionalInterface +public interface ConfigurationPropertiesBindHandlerAdvisor { + + /** + * Apply additional functionality to the source bind handler. + * @param bindHandler the source bind handler + * @return a replacement bind hander that delegates to the source and provides + * additional functionality + */ + BindHandler apply(BindHandler bindHandler); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java index 1575e5c9ea..d16dc05ff4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java @@ -19,6 +19,7 @@ package org.springframework.boot.context.properties; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.boot.context.properties.bind.BindHandler; @@ -126,9 +127,18 @@ class ConfigurationPropertiesBinder { handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0])); } + for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) { + handler = advisor.apply(handler); + } return handler; } + private List getBindHandlerAdvisors() { + return this.applicationContext + .getBeanProvider(ConfigurationPropertiesBindHandlerAdvisor.class) + .orderedStream().collect(Collectors.toList()); + } + private Binder getBinder() { if (this.binder == null) { this.binder = new Binder(getConfigurationPropertySources(), diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java index e10177d816..808e4152fb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ public abstract class AbstractBindHandler implements BindHandler { } @Override - public boolean onStart(ConfigurationPropertyName name, Bindable target, + public Bindable onStart(ConfigurationPropertyName name, Bindable target, BindContext context) { return this.parent.onStart(name, target, context); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java index 16ca63c1a4..e716cd82b8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java @@ -28,6 +28,12 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyS */ public interface BindContext { + /** + * Return the source binder that is performing the bind operation. + * @return the source binder + */ + Binder getBinder(); + /** * 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}. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java index 49983f30e6..525442626f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,14 +37,15 @@ public interface BindHandler { /** * Called when binding of an element starts but before any result has been determined. + * @param the bindable source type * @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, + default Bindable onStart(ConfigurationPropertyName name, Bindable target, BindContext context) { - return true; + return target; } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index 8b102b2fc9..98aabd30ca 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -214,7 +214,8 @@ public class Binder { BindHandler handler, Context context, boolean allowRecursiveBinding) { context.clearConfigurationProperty(); try { - if (!handler.onStart(name, target, context)) { + target = handler.onStart(name, target, context); + if (target == null) { return null; } Object bound = bindObject(name, target, handler, context, @@ -467,6 +468,11 @@ public class Binder { return this.converter; } + @Override + public Binder getBinder() { + return Binder.this; + } + @Override public int getDepth() { return this.depth; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindHandlerAdvisorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindHandlerAdvisorTests.java new file mode 100644 index 0000000000..a14732066d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindHandlerAdvisorTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; + +import org.junit.After; +import org.junit.Test; + +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.BindResult; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.support.TestPropertySourceUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertiesBindHandlerAdvisor}. + * + * @author Phillip Webb + */ +public class ConfigurationPropertiesBindHandlerAdvisorTests { + + private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + @After + public void cleanup() { + this.context.close(); + } + + @Test + public void loadWithoutConfigurationPropertiesBindHandlerAdvisor() { + load(WithoutConfigurationPropertiesBindHandlerAdvisor.class, + "foo.bar.default.content-type=text/plain", + "foo.bar.bindings.input.destination=d1", + "foo.bar.bindings.input.content-type=text/xml", + "foo.bar.bindings.output.destination=d2"); + BindingServiceProperties properties = this.context + .getBean(BindingServiceProperties.class); + BindingProperties input = properties.getBindings().get("input"); + assertThat(input.getDestination()).isEqualTo("d1"); + assertThat(input.getContentType()).isEqualTo("text/xml"); + BindingProperties output = properties.getBindings().get("output"); + assertThat(output.getDestination()).isEqualTo("d2"); + assertThat(output.getContentType()).isEqualTo("application/json"); + } + + @Test + public void loadWithConfigurationPropertiesBindHandlerAdvisor() { + load(WithConfigurationPropertiesBindHandlerAdvisor.class, + "foo.bar.default.content-type=text/plain", + "foo.bar.bindings.input.destination=d1", + "foo.bar.bindings.input.content-type=text/xml", + "foo.bar.bindings.output.destination=d2"); + BindingServiceProperties properties = this.context + .getBean(BindingServiceProperties.class); + BindingProperties input = properties.getBindings().get("input"); + assertThat(input.getDestination()).isEqualTo("d1"); + assertThat(input.getContentType()).isEqualTo("text/xml"); + BindingProperties output = properties.getBindings().get("output"); + assertThat(output.getDestination()).isEqualTo("d2"); + assertThat(output.getContentType()).isEqualTo("text/plain"); + } + + private AnnotationConfigApplicationContext load(Class configuration, + String... inlinedProperties) { + return load(new Class[] { configuration }, inlinedProperties); + } + + private AnnotationConfigApplicationContext load(Class[] configuration, + String... inlinedProperties) { + this.context.register(configuration); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, + inlinedProperties); + this.context.refresh(); + return this.context; + } + + @Configuration + @EnableConfigurationProperties(BindingServiceProperties.class) + static class WithoutConfigurationPropertiesBindHandlerAdvisor { + + } + + @Configuration + @EnableConfigurationProperties(BindingServiceProperties.class) + @Import(DefaultValuesConfigurationPropertiesBindHandlerAdvisor.class) + static class WithConfigurationPropertiesBindHandlerAdvisor { + + } + + static class DefaultValuesConfigurationPropertiesBindHandlerAdvisor + implements ConfigurationPropertiesBindHandlerAdvisor { + + @Override + public BindHandler apply(BindHandler bindHandler) { + return new DefaultValuesBindHandler(bindHandler); + } + + } + + static class DefaultValuesBindHandler extends AbstractBindHandler { + + private final Map mappings; + + DefaultValuesBindHandler(BindHandler bindHandler) { + super(bindHandler); + this.mappings = new LinkedHashMap<>(); + this.mappings.put(ConfigurationPropertyName.of("foo.bar.bindings"), + ConfigurationPropertyName.of("foo.bar.default")); + } + + @Override + public Bindable onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + ConfigurationPropertyName defaultName = getDefaultName(name); + if (defaultName != null) { + BindResult result = context.getBinder().bind(defaultName, target); + if (result.isBound()) { + return target.withExistingValue(result.get()); + } + } + return super.onStart(name, target, context); + + } + + private ConfigurationPropertyName getDefaultName(ConfigurationPropertyName name) { + for (Map.Entry mapping : this.mappings + .entrySet()) { + ConfigurationPropertyName from = mapping.getKey(); + ConfigurationPropertyName to = mapping.getValue(); + if (name.getNumberOfElements() == from.getNumberOfElements() + 1 + && from.isParentOf(name)) { + return to; + } + } + return null; + } + + } + + @ConfigurationProperties("foo.bar") + static class BindingServiceProperties { + + private Map bindings = new TreeMap<>(); + + public Map getBindings() { + return this.bindings; + } + + } + + static class BindingProperties { + + private String destination; + + private String contentType = "application/json"; + + public String getDestination() { + return this.destination; + } + + public void setDestination(String destination) { + this.destination = destination; + } + + public String getContentType() { + return this.contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + } + +}