From b10e2f3a051ec1de1fb17ae08e42c1f0c0c9687c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 25 Apr 2017 11:30:04 -0700 Subject: [PATCH 01/19] Deprecate relaxed property concepts Deprecate `RelaxedDataBinder`, `RelaxedPropertyResolver` and related classes in preparation for the updated configuration properties binder work. See gh-9000 --- .../boot/bind/DefaultPropertyNamePatternsMatcher.java | 1 + .../java/org/springframework/boot/bind/InetAddressEditor.java | 3 ++- .../springframework/boot/bind/OriginCapablePropertyValue.java | 3 ++- .../boot/bind/PatternPropertyNamePatternsMatcher.java | 3 ++- .../boot/bind/PropertiesConfigurationFactory.java | 1 + .../springframework/boot/bind/PropertyNamePatternsMatcher.java | 1 + .../java/org/springframework/boot/bind/PropertyOrigin.java | 3 ++- .../org/springframework/boot/bind/PropertySourceUtils.java | 1 + .../org/springframework/boot/bind/PropertySourcesBinder.java | 1 + .../boot/bind/PropertySourcesPropertyValues.java | 1 + .../boot/bind/RelaxedBindingNotWritablePropertyException.java | 3 ++- .../springframework/boot/bind/RelaxedConversionService.java | 1 + .../java/org/springframework/boot/bind/RelaxedDataBinder.java | 1 + .../main/java/org/springframework/boot/bind/RelaxedNames.java | 1 + .../org/springframework/boot/bind/RelaxedPropertyResolver.java | 3 ++- .../springframework/boot/bind/StringToCharArrayConverter.java | 3 ++- .../springframework/boot/bind/YamlConfigurationFactory.java | 1 + .../boot/bind/YamlJavaBeanPropertyConstructor.java | 1 + .../ConfigurationPropertiesBindingPostProcessorRegistrar.java | 2 +- .../EnableConfigurationPropertiesImportSelector.java | 1 + 20 files changed, 27 insertions(+), 8 deletions(-) diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java b/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java index 58b80d2f93..66cab537c8 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java @@ -28,6 +28,7 @@ import java.util.Set; * @author Phillip Webb * @since 1.2.0 */ +@Deprecated class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher { private final char[] delimiters; diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/InetAddressEditor.java b/spring-boot/src/main/java/org/springframework/boot/bind/InetAddressEditor.java index 2e48d81557..2175fde7fc 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/InetAddressEditor.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/InetAddressEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * 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. @@ -26,6 +26,7 @@ import java.net.UnknownHostException; * * @author Dave Syer */ +@Deprecated public class InetAddressEditor extends PropertyEditorSupport { @Override diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/OriginCapablePropertyValue.java b/spring-boot/src/main/java/org/springframework/boot/bind/OriginCapablePropertyValue.java index cb1d96599d..9332ed48b6 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/OriginCapablePropertyValue.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/OriginCapablePropertyValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * 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. @@ -24,6 +24,7 @@ import org.springframework.core.env.PropertySource; * * @author Andy Wilkinson */ +@Deprecated class OriginCapablePropertyValue extends PropertyValue { private static final String ATTRIBUTE_PROPERTY_ORIGIN = "propertyOrigin"; diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PatternPropertyNamePatternsMatcher.java b/spring-boot/src/main/java/org/springframework/boot/bind/PatternPropertyNamePatternsMatcher.java index 390947fc64..b95883d5cc 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PatternPropertyNamePatternsMatcher.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PatternPropertyNamePatternsMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -27,6 +27,7 @@ import org.springframework.util.PatternMatchUtils; * @author Phillip Webb * @since 1.2.0 */ +@Deprecated class PatternPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher { private final String[] patterns; diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java index be8fd600d8..27bf28d6e0 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java @@ -51,6 +51,7 @@ import org.springframework.validation.Validator; * @param the target type * @author Dave Syer */ +@Deprecated public class PropertiesConfigurationFactory implements FactoryBean, MessageSourceAware, InitializingBean { diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertyNamePatternsMatcher.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertyNamePatternsMatcher.java index d82bd2d63a..7ac154149e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertyNamePatternsMatcher.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertyNamePatternsMatcher.java @@ -22,6 +22,7 @@ package org.springframework.boot.bind; * @author Phillip Webb * @since 1.2.0 */ +@Deprecated interface PropertyNamePatternsMatcher { PropertyNamePatternsMatcher ALL = new PropertyNamePatternsMatcher() { diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertyOrigin.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertyOrigin.java index 47cca7f63e..d07de560af 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertyOrigin.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertyOrigin.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * 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. @@ -25,6 +25,7 @@ import org.springframework.core.env.PropertySource; * @author Andy Wilkinson * @since 1.3.0 */ +@Deprecated public class PropertyOrigin { private final PropertySource source; diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourceUtils.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourceUtils.java index fb30e91d90..d9dd190954 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourceUtils.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourceUtils.java @@ -31,6 +31,7 @@ import org.springframework.core.env.PropertySources; * @see PropertySource * @see PropertySources */ +@Deprecated public abstract class PropertySourceUtils { /** diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesBinder.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesBinder.java index 8e59e80ea9..c775014693 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesBinder.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesBinder.java @@ -34,6 +34,7 @@ import org.springframework.validation.BindException; * @author Stephane Nicoll * @since 1.4.0 */ +@Deprecated public class PropertySourcesBinder { private PropertySources propertySources; diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java index ce0b73cab5..1fb4cc29ec 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java @@ -41,6 +41,7 @@ import org.springframework.validation.DataBinder; * @author Dave Syer * @author Phillip Webb */ +@Deprecated public class PropertySourcesPropertyValues implements PropertyValues { private static final Pattern COLLECTION_PROPERTY = Pattern diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedBindingNotWritablePropertyException.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedBindingNotWritablePropertyException.java index 81d4bc8944..747d42ebb1 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedBindingNotWritablePropertyException.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedBindingNotWritablePropertyException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * 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. @@ -26,6 +26,7 @@ import org.springframework.beans.NotWritablePropertyException; * @since 1.3.0 * @see RelaxedDataBinder */ +@Deprecated public class RelaxedBindingNotWritablePropertyException extends NotWritablePropertyException { diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedConversionService.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedConversionService.java index 5db206cc57..9e83127e60 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedConversionService.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedConversionService.java @@ -36,6 +36,7 @@ import org.springframework.util.Assert; * @author Stephane Nicoll * @since 1.1.0 */ +@Deprecated class RelaxedConversionService implements ConversionService { private final ConversionService conversionService; diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java index 48fe904bbc..3c025d7ca8 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java @@ -56,6 +56,7 @@ import org.springframework.validation.DataBinder; * @author Andy Wilkinson * @see RelaxedNames */ +@Deprecated public class RelaxedDataBinder extends DataBinder { private static final Object BLANK = new Object(); diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedNames.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedNames.java index bd77de3900..4b54935866 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedNames.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedNames.java @@ -32,6 +32,7 @@ import org.springframework.util.StringUtils; * @see RelaxedDataBinder * @see RelaxedPropertyResolver */ +@Deprecated public final class RelaxedNames implements Iterable { private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([^A-Z-])([A-Z])"); diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java index 327165a620..93ab74dece 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -30,6 +30,7 @@ import org.springframework.util.Assert; * @author Phillip Webb * @see RelaxedNames */ +@Deprecated public class RelaxedPropertyResolver implements PropertyResolver { private final PropertyResolver resolver; diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/StringToCharArrayConverter.java b/spring-boot/src/main/java/org/springframework/boot/bind/StringToCharArrayConverter.java index 1c3e20bcbb..17f9116564 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/StringToCharArrayConverter.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/StringToCharArrayConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -23,6 +23,7 @@ import org.springframework.core.convert.converter.Converter; * * @author Phillip Webb */ +@Deprecated class StringToCharArrayConverter implements Converter { @Override diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/YamlConfigurationFactory.java b/spring-boot/src/main/java/org/springframework/boot/bind/YamlConfigurationFactory.java index 63fdf39b5e..a9629da835 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/YamlConfigurationFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/YamlConfigurationFactory.java @@ -48,6 +48,7 @@ import org.springframework.validation.Validator; * @author Luke Taylor * @author Dave Syer */ +@Deprecated public class YamlConfigurationFactory implements FactoryBean, MessageSourceAware, InitializingBean { diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/YamlJavaBeanPropertyConstructor.java b/spring-boot/src/main/java/org/springframework/boot/bind/YamlJavaBeanPropertyConstructor.java index 6d0688342d..f3b726e492 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/YamlJavaBeanPropertyConstructor.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/YamlJavaBeanPropertyConstructor.java @@ -31,6 +31,7 @@ import org.yaml.snakeyaml.nodes.NodeId; * * @author Luke Taylor */ +@Deprecated public class YamlJavaBeanPropertyConstructor extends Constructor { private final Map, Map> properties = new HashMap<>(); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java index 459972fdde..87ac7cccf3 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * 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. diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java index 12e0f1a996..96cb5cc410 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java @@ -45,6 +45,7 @@ import org.springframework.util.StringUtils; * @author Christian Dupuis * @author Stephane Nicoll */ +@Deprecated class EnableConfigurationPropertiesImportSelector implements ImportSelector { @Override From dbc7e938c8cf243e70b4e66eb67dfa95f43f3688 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 17 Apr 2017 11:41:53 -0700 Subject: [PATCH 02/19] Restructor and improve Origin support Move the `Origin` and related classes from `o.s.boot.env` to `o.s.boot.orgin` and extend support. The concept of an origin can now be used outside of just the Spring Environment abstraction. Closes gh-9001 --- .../boot/env/OriginCapablePropertySource.java | 39 ------ .../env/OriginTrackedMapPropertySource.java | 11 +- .../env/OriginTrackedPropertiesLoader.java | 7 +- .../boot/env/OriginTrackedYamlLoader.java | 11 +- .../boot/env/PropertySourcesLoader.java | 4 +- .../springframework/boot/origin/Origin.java | 56 +++++++++ .../boot/origin/OriginLookup.java | 59 +++++++++ .../OriginProvider.java} | 21 ++-- .../{env => origin}/OriginTrackedValue.java | 40 ++++-- .../boot/origin/PropertySourceOrigin.java | 81 +++++++++++++ .../TextResourceOrigin.java} | 12 +- .../boot/origin/package-info.java | 21 ++++ .../OriginTrackedMapPropertySourceTests.java | 15 ++- .../OriginTrackedPropertiesLoaderTests.java | 5 +- .../env/OriginTrackedYamlLoaderTests.java | 5 +- .../boot/origin/MockOrigin.java | 60 +++++++++ .../boot/origin/OriginLookupTests.java | 63 ++++++++++ .../boot/origin/OriginTests.java | 90 ++++++++++++++ .../boot/origin/OriginTrackedValueTests.java | 78 ++++++++++++ .../origin/PropertySourceOriginTests.java | 114 ++++++++++++++++++ .../TextResourceOriginTests.java} | 35 +++--- 21 files changed, 718 insertions(+), 109 deletions(-) delete mode 100644 spring-boot/src/main/java/org/springframework/boot/env/OriginCapablePropertySource.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/origin/Origin.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/origin/OriginLookup.java rename spring-boot/src/main/java/org/springframework/boot/{env/PropertyOrigin.java => origin/OriginProvider.java} (60%) rename spring-boot/src/main/java/org/springframework/boot/{env => origin}/OriginTrackedValue.java (68%) create mode 100644 spring-boot/src/main/java/org/springframework/boot/origin/PropertySourceOrigin.java rename spring-boot/src/main/java/org/springframework/boot/{env/TextResourcePropertyOrigin.java => origin/TextResourceOrigin.java} (90%) create mode 100644 spring-boot/src/main/java/org/springframework/boot/origin/package-info.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/origin/MockOrigin.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/origin/OriginLookupTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/origin/OriginTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/origin/OriginTrackedValueTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/origin/PropertySourceOriginTests.java rename spring-boot/src/test/java/org/springframework/boot/{env/TextResourcePropertyOriginTests.java => origin/TextResourceOriginTests.java} (78%) diff --git a/spring-boot/src/main/java/org/springframework/boot/env/OriginCapablePropertySource.java b/spring-boot/src/main/java/org/springframework/boot/env/OriginCapablePropertySource.java deleted file mode 100644 index 34d3032426..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/env/OriginCapablePropertySource.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.env; - -import org.springframework.core.env.PropertySource; - -/** - * An additional interface that may be implemented by a {@link PropertySource} that can - * return origin information. For example a property source that's backed by a file may - * return origin information for line and column numbers. - * - * @author Phillip Webb - * @since 2.0.0 - */ -public interface OriginCapablePropertySource { - - /** - * Return the origin of the given property name or {@code null} if the origin cannot - * be determined. - * @param name the property name - * @return the origin of the property or {@code null} - */ - PropertyOrigin getPropertyOrigin(String name); - -} diff --git a/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedMapPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedMapPropertySource.java index 2ffeb97dd1..5ffb63ff6a 100644 --- a/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedMapPropertySource.java +++ b/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedMapPropertySource.java @@ -18,18 +18,21 @@ package org.springframework.boot.env; import java.util.Map; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; +import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.core.env.MapPropertySource; /** - * {@link OriginCapablePropertySource} backed by a {@link Map} containing - * {@link OriginTrackedValue OriginTrackedValues}. + * {@link OriginLookup} backed by a {@link Map} containing {@link OriginTrackedValue + * OriginTrackedValues}. * * @author Madhura Bhave * @author Phillip Webb * @see OriginTrackedValue */ class OriginTrackedMapPropertySource extends MapPropertySource - implements OriginCapablePropertySource { + implements OriginLookup { @SuppressWarnings({ "unchecked", "rawtypes" }) OriginTrackedMapPropertySource(String name, Map source) { @@ -46,7 +49,7 @@ class OriginTrackedMapPropertySource extends MapPropertySource } @Override - public PropertyOrigin getPropertyOrigin(String name) { + public Origin getOrigin(String name) { Object value = super.getProperty(name); if (value instanceof OriginTrackedValue) { return ((OriginTrackedValue) value).getOrigin(); diff --git a/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java b/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java index 42aafdc326..3e0e06c1e2 100644 --- a/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java @@ -23,7 +23,10 @@ import java.io.LineNumberReader; import java.util.LinkedHashMap; import java.util.Map; -import org.springframework.boot.env.TextResourcePropertyOrigin.Location; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.boot.origin.TextResourceOrigin; +import org.springframework.boot.origin.TextResourceOrigin.Location; import org.springframework.core.io.Resource; import org.springframework.util.Assert; @@ -129,7 +132,7 @@ class OriginTrackedPropertiesLoader { buffer.append(reader.getCharacter()); reader.read(); } - PropertyOrigin origin = new TextResourcePropertyOrigin(this.resource, location); + Origin origin = new TextResourceOrigin(this.resource, location); return OriginTrackedValue.of(buffer.toString().trim(), origin); } diff --git a/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java b/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java index 45a3cd8d45..5e8a3996f3 100644 --- a/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java @@ -37,7 +37,10 @@ import org.yaml.snakeyaml.representer.Representer; import org.yaml.snakeyaml.resolver.Resolver; import org.springframework.beans.factory.config.YamlProcessor; -import org.springframework.boot.env.TextResourcePropertyOrigin.Location; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.boot.origin.TextResourceOrigin; +import org.springframework.boot.origin.TextResourceOrigin.Location; import org.springframework.boot.yaml.SpringProfileDocumentMatcher; import org.springframework.core.io.Resource; @@ -106,14 +109,14 @@ class OriginTrackedYamlLoader extends YamlProcessor { } private Object constructTrackedObject(Node node, Object value) { - PropertyOrigin origin = getOrigin(node); + Origin origin = getOrigin(node); return OriginTrackedValue.of(value, origin); } - private PropertyOrigin getOrigin(Node node) { + private Origin getOrigin(Node node) { Mark mark = node.getStartMark(); Location location = new Location(mark.getLine(), mark.getColumn()); - return new TextResourcePropertyOrigin(OriginTrackedYamlLoader.this.resource, + return new TextResourceOrigin(OriginTrackedYamlLoader.this.resource, location); } diff --git a/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java b/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java index 81057c6cf2..938016368f 100644 --- a/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java @@ -33,8 +33,8 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * Utility that can be used update {@link MutablePropertySources} using - * {@link PropertySourceLoader}s. + * Utility that can be used to update {@link MutablePropertySources} using + * {@link PropertySourceLoader PropertySourceLoaders}. * * @author Phillip Webb */ diff --git a/spring-boot/src/main/java/org/springframework/boot/origin/Origin.java b/spring-boot/src/main/java/org/springframework/boot/origin/Origin.java new file mode 100644 index 0000000000..b0c3a6c4f6 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/origin/Origin.java @@ -0,0 +1,56 @@ +/* + * 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.origin; + +import java.io.File; + +/** + * Interface that uniquely represents the origin of an item. For example, a item loaded + * from a {@link File} may have an origin made up of the file name along with line/column + * numbers. + *

+ * Implementations must provide sensible {@code hashCode()}, {@code equals(...)} and + * {@code #toString()} implementations. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.0.0 + * @see OriginProvider + */ +public interface Origin { + + /** + * Find the {@link Origin} that an object originated from. Checks if the source object + * is a {@link OriginProvider} and also searches exception stacks. + * @param source the source object or {@code null} + * @return an optional {@link Origin} + */ + static Origin from(Object source) { + if (source instanceof Origin) { + return (Origin) source; + } + Origin origin = null; + if (source != null && source instanceof OriginProvider) { + origin = ((OriginProvider) source).getOrigin(); + } + if (origin == null && source != null && source instanceof Throwable) { + return from(((Throwable) source).getCause()); + } + return origin; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/origin/OriginLookup.java b/spring-boot/src/main/java/org/springframework/boot/origin/OriginLookup.java new file mode 100644 index 0000000000..2859178c0b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/origin/OriginLookup.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.origin; + +/** + * An interface that may be implemented by an object that can lookup {@link Origin} + * information from a given key. Can be used to add origin support to existing classes. + * + * @param The lookup key type + * @author Phillip Webb + * @since 2.0.0 + */ +public interface OriginLookup { + + /** + * Return the origin of the given key or {@code null} if the origin cannot be + * determined. + * @param key the key to lookup + * @return the origin of the key or {@code null} + */ + Origin getOrigin(K key); + + /** + * Attempt to lookup the origin from the given source. If the source is not a + * {@link OriginLookup} or if an exception occurs during lookup then {@code null} is + * returned. + * @param source the source object + * @param key the key to lookup + * @param the key type + * @return an {@link Origin} or {@code null} + */ + @SuppressWarnings("unchecked") + static Origin getOrigin(Object source, K key) { + if (!(source instanceof OriginLookup)) { + return null; + } + try { + return ((OriginLookup) source).getOrigin(key); + } + catch (Throwable ex) { + return null; + } + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/env/PropertyOrigin.java b/spring-boot/src/main/java/org/springframework/boot/origin/OriginProvider.java similarity index 60% rename from spring-boot/src/main/java/org/springframework/boot/env/PropertyOrigin.java rename to spring-boot/src/main/java/org/springframework/boot/origin/OriginProvider.java index 9d685f9aac..19306277de 100644 --- a/spring-boot/src/main/java/org/springframework/boot/env/PropertyOrigin.java +++ b/spring-boot/src/main/java/org/springframework/boot/origin/OriginProvider.java @@ -14,22 +14,21 @@ * limitations under the License. */ -package org.springframework.boot.env; - -import java.io.File; +package org.springframework.boot.origin; /** - * Interface that uniquely represents the origin of a property. For example, a property - * loaded from a {@link File} may have an origin made up of the file name along with - * line/column numbers. - *

- * Implementations must provide sensible {@code hashCode()}, {@code equals(...)} and - * {@code #toString()} implementations. + * Interface to provide access to the origin of an item. * - * @author Madhura Bhave * @author Phillip Webb * @since 2.0.0 + * @see Origin */ -public interface PropertyOrigin { +public interface OriginProvider { + + /** + * Return the source origin or {@code null} if the origin is not known. + * @return the origin or {@code null} + */ + Origin getOrigin(); } diff --git a/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedValue.java b/spring-boot/src/main/java/org/springframework/boot/origin/OriginTrackedValue.java similarity index 68% rename from spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedValue.java rename to spring-boot/src/main/java/org/springframework/boot/origin/OriginTrackedValue.java index cfa9a867b8..7b814fa273 100644 --- a/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedValue.java +++ b/spring-boot/src/main/java/org/springframework/boot/origin/OriginTrackedValue.java @@ -14,41 +14,51 @@ * limitations under the License. */ -package org.springframework.boot.env; +package org.springframework.boot.origin; + +import org.springframework.util.ObjectUtils; /** - * Wrapper class for an Object {@code value} and {@link PropertyOrigin origin}. + * An wrapper for a {@link Object} value and {@link Origin}. * * @author Madhura Bhave - * @see OriginTrackedMapPropertySource + * @author Phillip Webb + * @since 2.0.0 + * @see #of(Object) + * @see #of(Object, Origin) */ -class OriginTrackedValue { +public class OriginTrackedValue implements OriginProvider { private final Object value; - private final PropertyOrigin origin; + private final Origin origin; - OriginTrackedValue(Object value, PropertyOrigin origin) { + private OriginTrackedValue(Object value, Origin origin) { this.value = value; this.origin = origin; } + /** + * Return the tracked value. + * @return the tracked value + */ public Object getValue() { return this.value; } - public PropertyOrigin getOrigin() { + @Override + public Origin getOrigin() { return this.origin; } @Override public String toString() { - return this.value.toString(); + return (this.value == null ? null : this.value.toString()); } @Override public int hashCode() { - return this.value.hashCode(); + return ObjectUtils.nullSafeHashCode(this.value); } @Override @@ -56,7 +66,11 @@ class OriginTrackedValue { if (obj == null || obj.getClass() != getClass()) { return false; } - return this.value.equals(((OriginTrackedValue) obj).value); + return ObjectUtils.nullSafeEquals(this.value, ((OriginTrackedValue) obj).value); + } + + public static OriginTrackedValue of(Object value) { + return of(value, null); } /** @@ -65,10 +79,10 @@ class OriginTrackedValue { * the resulting {@link OriginTrackedValue}. * @param value the source value * @param origin the origin - * @return an {@link OriginTrackedValue} or {@code null} if the source value was + * @return a {@link OriginTrackedValue} or {@code null} if the source value was * {@code null}. */ - public static OriginTrackedValue of(Object value, PropertyOrigin origin) { + public static OriginTrackedValue of(Object value, Origin origin) { if (value == null) { return null; } @@ -84,7 +98,7 @@ class OriginTrackedValue { private static class OriginTrackedCharSequence extends OriginTrackedValue implements CharSequence { - OriginTrackedCharSequence(CharSequence value, PropertyOrigin origin) { + OriginTrackedCharSequence(CharSequence value, Origin origin) { super(value, origin); } diff --git a/spring-boot/src/main/java/org/springframework/boot/origin/PropertySourceOrigin.java b/spring-boot/src/main/java/org/springframework/boot/origin/PropertySourceOrigin.java new file mode 100644 index 0000000000..c7a4258726 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/origin/PropertySourceOrigin.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.origin; + +import org.springframework.core.env.PropertySource; +import org.springframework.util.Assert; + +/** + * {@link Origin} from a {@link PropertySource}. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public class PropertySourceOrigin implements Origin { + + private final PropertySource propertySource; + + private final String propertyName; + + /** + * Create a new {@link PropertySourceOrigin} instance. + * @param propertySource the origin property source + * @param propertyName the origin property name + */ + public PropertySourceOrigin(PropertySource propertySource, String propertyName) { + Assert.notNull(propertySource, "PropertySource must not be null"); + Assert.hasLength(propertyName, "PropertyName must not be empty"); + this.propertySource = propertySource; + this.propertyName = propertyName; + } + + /** + * Return the origin {@link PropertySource}. + * @return the origin property source + */ + public PropertySource getPropertySource() { + return this.propertySource; + } + + /** + * Return the origin property name. + * @return the origin property name + */ + public String getPropertyName() { + return this.propertyName; + } + + @Override + public String toString() { + return "\"" + this.propertyName + "\" from property source \"" + + this.propertySource.getName() + "\""; + } + + /** + * Get a {@link Origin} for the given {@link PropertySource} and {@code propertyName}. + * Will either return an {@link OriginLookup} result or a + * {@link PropertySourceOrigin}. + * @param propertySource the origin property source + * @param name the property name + * @return the property origin + */ + public static Origin get(PropertySource propertySource, String name) { + Origin origin = OriginLookup.getOrigin(propertySource, name); + return (origin != null ? origin : new PropertySourceOrigin(propertySource, name)); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/env/TextResourcePropertyOrigin.java b/spring-boot/src/main/java/org/springframework/boot/origin/TextResourceOrigin.java similarity index 90% rename from spring-boot/src/main/java/org/springframework/boot/env/TextResourcePropertyOrigin.java rename to spring-boot/src/main/java/org/springframework/boot/origin/TextResourceOrigin.java index e588429481..851f6bb5dd 100644 --- a/spring-boot/src/main/java/org/springframework/boot/env/TextResourcePropertyOrigin.java +++ b/spring-boot/src/main/java/org/springframework/boot/origin/TextResourceOrigin.java @@ -14,26 +14,26 @@ * limitations under the License. */ -package org.springframework.boot.env; +package org.springframework.boot.origin; import org.springframework.core.io.Resource; import org.springframework.util.ObjectUtils; /** - * {@link PropertyOrigin} for an item loaded from a text resource. Provides access to the + * {@link Origin} for an item loaded from a text resource. Provides access to the * original {@link Resource} that loaded the text and a {@link Location} within it. * * @author Madhura Bhave * @author Phillip Webb * @since 2.0.0 */ -public class TextResourcePropertyOrigin implements PropertyOrigin { +public class TextResourceOrigin implements Origin { private final Resource resource; private final Location location; - public TextResourcePropertyOrigin(Resource resource, Location location) { + public TextResourceOrigin(Resource resource, Location location) { this.resource = resource; this.location = location; } @@ -70,8 +70,8 @@ public class TextResourcePropertyOrigin implements PropertyOrigin { if (obj == null) { return false; } - if (obj instanceof TextResourcePropertyOrigin) { - TextResourcePropertyOrigin other = (TextResourcePropertyOrigin) obj; + if (obj instanceof TextResourceOrigin) { + TextResourceOrigin other = (TextResourceOrigin) obj; boolean result = true; result = result && ObjectUtils.nullSafeEquals(this.resource, other.resource); result = result && ObjectUtils.nullSafeEquals(this.location, other.location); diff --git a/spring-boot/src/main/java/org/springframework/boot/origin/package-info.java b/spring-boot/src/main/java/org/springframework/boot/origin/package-info.java new file mode 100644 index 0000000000..d1c03ed9de --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/origin/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for item origin tracking. + * @see org.springframework.boot.origin.Origin + */ +package org.springframework.boot.origin; diff --git a/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedMapPropertySourceTests.java b/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedMapPropertySourceTests.java index bf3e33aa3e..3737726dad 100644 --- a/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedMapPropertySourceTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedMapPropertySourceTests.java @@ -21,6 +21,9 @@ import java.util.Map; import org.junit.Test; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginTrackedValue; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -37,7 +40,7 @@ public class OriginTrackedMapPropertySourceTests { private OriginTrackedMapPropertySource source = new OriginTrackedMapPropertySource( "test", this.map); - private PropertyOrigin origin = mock(PropertyOrigin.class); + private Origin origin = mock(Origin.class); @Test public void getPropertyWhenMissingShouldReturnNull() throws Exception { @@ -52,25 +55,25 @@ public class OriginTrackedMapPropertySourceTests { @Test public void getPropertyWhenTrackedShouldReturnValue() throws Exception { - this.map.put("test", new OriginTrackedValue("foo", this.origin)); + this.map.put("test", OriginTrackedValue.of("foo", this.origin)); assertThat(this.source.getProperty("test")).isEqualTo("foo"); } @Test public void getPropertyOriginWhenMissingShouldReturnNull() throws Exception { - assertThat(this.source.getPropertyOrigin("test")).isNull(); + assertThat(this.source.getOrigin("test")).isNull(); } @Test public void getPropertyOriginWhenNonTrackedShouldReturnNull() throws Exception { this.map.put("test", "foo"); - assertThat(this.source.getPropertyOrigin("test")).isNull(); + assertThat(this.source.getOrigin("test")).isNull(); } @Test public void getPropertyOriginWhenTrackedShouldReturnOrigin() throws Exception { - this.map.put("test", new OriginTrackedValue("foo", this.origin)); - assertThat(this.source.getPropertyOrigin("test")).isEqualTo(this.origin); + this.map.put("test", OriginTrackedValue.of("foo", this.origin)); + assertThat(this.source.getOrigin("test")).isEqualTo(this.origin); } } diff --git a/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java b/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java index 3619cac0e8..3617b5ad9c 100644 --- a/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java @@ -22,6 +22,8 @@ import java.util.Properties; import org.junit.Before; import org.junit.Test; +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.boot.origin.TextResourceOrigin; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; @@ -234,7 +236,8 @@ public class OriginTrackedPropertiesLoaderTests { if (value == null) { return null; } - return ((TextResourcePropertyOrigin) value.getOrigin()).getLocation().toString(); + return ((TextResourceOrigin) value.getOrigin()).getLocation() + .toString(); } } diff --git a/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java b/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java index ad4b3e1a5b..094f7a30a0 100644 --- a/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java @@ -21,6 +21,8 @@ import java.util.Map; import org.junit.Before; import org.junit.Test; +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.boot.origin.TextResourceOrigin; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -120,7 +122,8 @@ public class OriginTrackedYamlLoaderTests { } private String getLocation(OriginTrackedValue value) { - return ((TextResourcePropertyOrigin) value.getOrigin()).getLocation().toString(); + return ((TextResourceOrigin) value.getOrigin()).getLocation() + .toString(); } } diff --git a/spring-boot/src/test/java/org/springframework/boot/origin/MockOrigin.java b/spring-boot/src/test/java/org/springframework/boot/origin/MockOrigin.java new file mode 100644 index 0000000000..7f81298e75 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/origin/MockOrigin.java @@ -0,0 +1,60 @@ +/* + * 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.origin; + +import org.springframework.util.Assert; + +/** + * Mock {@link Origin} implementation used for testing. + * + * @author Phillip Webb + */ +public final class MockOrigin implements Origin { + + private final String value; + + private MockOrigin(String value) { + Assert.notNull(value, "Value must not be null"); + this.value = value; + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.value.equals(((MockOrigin) obj).value); + } + + @Override + public String toString() { + return this.value; + } + + public static Origin of(String value) { + return (value == null ? null : new MockOrigin(value)); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/origin/OriginLookupTests.java b/spring-boot/src/test/java/org/springframework/boot/origin/OriginLookupTests.java new file mode 100644 index 0000000000..1413413bb5 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/origin/OriginLookupTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.origin; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link OriginLookup}. + * + * @author Phillip Webb + */ +public class OriginLookupTests { + + @Test + public void getOriginWhenSourceIsNullShouldReturnNull() throws Exception { + assertThat(OriginLookup.getOrigin(null, "foo")).isNull(); + } + + @Test + public void getOriginWhenSourceIsNotLookupShouldReturnLookupOrigin() + throws Exception { + Object source = new Object(); + assertThat(OriginLookup.getOrigin(source, "foo")).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + public void getOriginWhenSourceIsLookupShouldReturnLookupOrigin() throws Exception { + OriginLookup source = mock(OriginLookup.class); + Origin origin = MockOrigin.of("bar"); + given(source.getOrigin("foo")).willReturn(origin); + assertThat(OriginLookup.getOrigin(source, "foo")).isEqualTo(origin); + } + + @Test + @SuppressWarnings("unchecked") + public void getOriginWhenSourceLookupThrowsAndErrorShouldReturnNull() + throws Exception { + OriginLookup source = mock(OriginLookup.class); + willThrow(RuntimeException.class).given(source).getOrigin("foo"); + assertThat(OriginLookup.getOrigin(source, "foo")).isNull(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/origin/OriginTests.java b/spring-boot/src/test/java/org/springframework/boot/origin/OriginTests.java new file mode 100644 index 0000000000..bb9b2c56d8 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/origin/OriginTests.java @@ -0,0 +1,90 @@ +/* + * 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.origin; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link Origin}. + * + * @author Phillip Webb + */ +public class OriginTests { + + @Test + public void fromWhenSourceIsNullShouldReturnNull() throws Exception { + assertThat(Origin.from(null)).isNull(); + } + + @Test + public void fromWhenSourceIsRegularObjectShouldReturnNull() throws Exception { + Object source = new Object(); + assertThat(Origin.from(source)).isNull(); + } + + @Test + public void fromWhenSourceIsOriginShouldReturnSource() throws Exception { + Origin origin = mock(Origin.class); + assertThat(Origin.from(origin)).isEqualTo(origin); + } + + @Test + public void fromWhenSourceIsOriginProviderShouldReturnProvidedOrigin() + throws Exception { + Origin origin = mock(Origin.class); + OriginProvider originProvider = mock(OriginProvider.class); + given(originProvider.getOrigin()).willReturn(origin); + assertThat(Origin.from(origin)).isEqualTo(origin); + } + + @Test + public void fromWhenSourceIsThrowableShouldUseCause() throws Exception { + Origin origin = mock(Origin.class); + Exception exception = new RuntimeException(new TestException(origin, null)); + assertThat(Origin.from(exception)).isEqualTo(origin); + } + + @Test + public void fromWhenSourceIsThrowableAndOriginProviderThatReturnsNullShouldUseCause() + throws Exception { + Origin origin = mock(Origin.class); + Exception exception = new TestException(null, new TestException(origin, null)); + assertThat(Origin.from(exception)).isEqualTo(origin); + } + + private static class TestException extends RuntimeException + implements OriginProvider { + + private final Origin origin; + + TestException(Origin origin, Throwable cause) { + super(cause); + this.origin = origin; + } + + @Override + public Origin getOrigin() { + return this.origin; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/origin/OriginTrackedValueTests.java b/spring-boot/src/test/java/org/springframework/boot/origin/OriginTrackedValueTests.java new file mode 100644 index 0000000000..0f3ef77b98 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/origin/OriginTrackedValueTests.java @@ -0,0 +1,78 @@ +/* + * 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.origin; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link OriginTrackedValue}. + * + * @author Phillip Webb + */ +public class OriginTrackedValueTests { + + @Test + public void getValueShouldReturnValue() throws Exception { + Object value = new Object(); + assertThat(OriginTrackedValue.of(value).getValue()).isEqualTo(value); + } + + @Test + public void getOriginShouldReturnOrigin() throws Exception { + Object value = new Object(); + Origin origin = mock(Origin.class); + assertThat(OriginTrackedValue.of(value, origin).getOrigin()).isEqualTo(origin); + } + + @Test + public void toStringShouldReturnValueToString() throws Exception { + Object value = new Object(); + assertThat(OriginTrackedValue.of(value).toString()).isEqualTo(value.toString()); + } + + @Test + public void hashCodeAndEqualsShouldIgnoreOrigin() throws Exception { + Object value1 = new Object(); + OriginTrackedValue tracked1 = OriginTrackedValue.of(value1); + OriginTrackedValue tracked2 = OriginTrackedValue.of(value1, mock(Origin.class)); + OriginTrackedValue tracked3 = OriginTrackedValue.of(new Object()); + assertThat(tracked1.hashCode()).isEqualTo(tracked2.hashCode()); + assertThat(tracked1).isEqualTo(tracked1).isEqualTo(tracked2) + .isNotEqualTo(tracked3); + } + + @Test + public void ofWhenValueIsNullShouldReturnNull() throws Exception { + assertThat(OriginTrackedValue.of(null)).isNull(); + assertThat(OriginTrackedValue.of(null, mock(Origin.class))).isNull(); + } + + @Test + public void ofWhenValueIsCharSequenceShouldReturnCharSequence() throws Exception { + String value = "foo"; + OriginTrackedValue tracked = OriginTrackedValue.of(value); + assertThat(tracked).isInstanceOf(CharSequence.class); + CharSequence charSequence = (CharSequence) tracked; + assertThat(charSequence.length()).isEqualTo(value.length()); + assertThat(charSequence.charAt(0)).isEqualTo(value.charAt(0)); + assertThat(charSequence.subSequence(0, 1)).isEqualTo(value.subSequence(0, 1)); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/origin/PropertySourceOriginTests.java b/spring-boot/src/test/java/org/springframework/boot/origin/PropertySourceOriginTests.java new file mode 100644 index 0000000000..8d29961b9c --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/origin/PropertySourceOriginTests.java @@ -0,0 +1,114 @@ +/* + * 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.origin; + +import java.util.HashMap; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; + +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.withSettings; + +/** + * Tests for {@link PropertySourceOrigin}. + * + * @author Phillip Webb + */ +public class PropertySourceOriginTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenPropertySourceIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("PropertySource must not be null"); + new PropertySourceOrigin(null, "name"); + } + + @Test + public void createWhenPropertyNameIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("PropertyName must not be empty"); + new PropertySourceOrigin(mock(PropertySource.class), null); + } + + @Test + public void createWhenPropertyNameIsEmptyShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("PropertyName must not be empty"); + new PropertySourceOrigin(mock(PropertySource.class), ""); + } + + @Test + public void getPropertySourceShouldReturnPropertySource() throws Exception { + MapPropertySource propertySource = new MapPropertySource("test", new HashMap<>()); + PropertySourceOrigin origin = new PropertySourceOrigin(propertySource, "foo"); + assertThat(origin.getPropertySource()).isEqualTo(propertySource); + } + + @Test + public void getPropertyNameShouldReturnPropertyName() throws Exception { + MapPropertySource propertySource = new MapPropertySource("test", new HashMap<>()); + PropertySourceOrigin origin = new PropertySourceOrigin(propertySource, "foo"); + assertThat(origin.getPropertyName()).isEqualTo("foo"); + } + + @Test + public void toStringShouldShowDetails() throws Exception { + MapPropertySource propertySource = new MapPropertySource("test", new HashMap<>()); + PropertySourceOrigin origin = new PropertySourceOrigin(propertySource, "foo"); + assertThat(origin.toString()).isEqualTo("\"foo\" from property source \"test\""); + } + + @Test + @SuppressWarnings("unchecked") + public void getWhenPropertySourceSupportsOriginLookupShouldReturnOrigin() + throws Exception { + Origin origin = mock(Origin.class); + PropertySource propertySource = mock(PropertySource.class, + withSettings().extraInterfaces(OriginLookup.class)); + OriginLookup originCapablePropertySource = (OriginLookup) propertySource; + given(originCapablePropertySource.getOrigin("foo")).willReturn(origin); + assertThat(PropertySourceOrigin.get(propertySource, "foo")).isSameAs(origin); + } + + @Test + public void getWhenPropertySourceSupportsOriginLookupButNoOriginShouldWrap() + throws Exception { + PropertySource propertySource = mock(PropertySource.class, + withSettings().extraInterfaces(OriginLookup.class)); + assertThat(PropertySourceOrigin.get(propertySource, "foo")) + .isInstanceOf(PropertySourceOrigin.class); + } + + @Test + public void getWhenPropertySourceIsNotOriginAwareShouldWrap() throws Exception { + MapPropertySource propertySource = new MapPropertySource("test", new HashMap<>()); + PropertySourceOrigin origin = new PropertySourceOrigin(propertySource, "foo"); + assertThat(origin.getPropertySource()).isEqualTo(propertySource); + assertThat(origin.getPropertyName()).isEqualTo("foo"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/env/TextResourcePropertyOriginTests.java b/spring-boot/src/test/java/org/springframework/boot/origin/TextResourceOriginTests.java similarity index 78% rename from spring-boot/src/test/java/org/springframework/boot/env/TextResourcePropertyOriginTests.java rename to spring-boot/src/test/java/org/springframework/boot/origin/TextResourceOriginTests.java index 92f6543c65..6b7d0d1a8d 100644 --- a/spring-boot/src/test/java/org/springframework/boot/env/TextResourcePropertyOriginTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/origin/TextResourceOriginTests.java @@ -14,47 +14,45 @@ * limitations under the License. */ -package org.springframework.boot.env; +package org.springframework.boot.origin; import org.junit.Test; -import org.springframework.boot.env.TextResourcePropertyOrigin.Location; +import org.springframework.boot.origin.TextResourceOrigin.Location; import org.springframework.core.io.ClassPathResource; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link TextResourcePropertyOrigin}. + * Tests for {@link TextResourceOrigin}. * * @author Phillip Webb */ -public class TextResourcePropertyOriginTests { +public class TextResourceOriginTests { @Test public void createWithNullResourceShouldSetNullResource() throws Exception { - TextResourcePropertyOrigin origin = new TextResourcePropertyOrigin(null, null); + TextResourceOrigin origin = new TextResourceOrigin(null, null); assertThat(origin.getResource()).isNull(); } @Test public void createWithNullLocationShouldSetNullLocation() throws Exception { - TextResourcePropertyOrigin origin = new TextResourcePropertyOrigin(null, null); + TextResourceOrigin origin = new TextResourceOrigin(null, null); assertThat(origin.getLocation()).isNull(); } @Test public void getResourceShouldReturnResource() throws Exception { ClassPathResource resource = new ClassPathResource("foo.txt"); - TextResourcePropertyOrigin origin = new TextResourcePropertyOrigin(resource, - null); + TextResourceOrigin origin = new TextResourceOrigin(resource, null); assertThat(origin.getResource()).isEqualTo(resource); } @Test public void getLocationShouldReturnLocation() throws Exception { Location location = new Location(1, 2); - TextResourcePropertyOrigin origin = new TextResourcePropertyOrigin(null, - location); + TextResourceOrigin origin = new TextResourceOrigin(null, location); assertThat(origin.getLocation()).isEqualTo(location); } @@ -81,24 +79,21 @@ public class TextResourcePropertyOriginTests { public void toStringShouldReturnNiceString() throws Exception { ClassPathResource resource = new ClassPathResource("foo.txt"); Location location = new Location(1, 2); - TextResourcePropertyOrigin origin = new TextResourcePropertyOrigin(resource, - location); + TextResourceOrigin origin = new TextResourceOrigin(resource, location); assertThat(origin.toString()).isEqualTo("class path resource [foo.txt]:2:3"); } @Test public void toStringWhenResourceIsNullShouldReturnNiceString() throws Exception { Location location = new Location(1, 2); - TextResourcePropertyOrigin origin = new TextResourcePropertyOrigin(null, - location); + TextResourceOrigin origin = new TextResourceOrigin(null, location); assertThat(origin.toString()).isEqualTo("unknown resource [?]:2:3"); } @Test public void toStringWhenLocationIsNullShouldReturnNiceString() throws Exception { ClassPathResource resource = new ClassPathResource("foo.txt"); - TextResourcePropertyOrigin origin = new TextResourcePropertyOrigin(resource, - null); + TextResourceOrigin origin = new TextResourceOrigin(resource, null); assertThat(origin.toString()).isEqualTo("class path resource [foo.txt]"); } @@ -117,13 +112,13 @@ public class TextResourcePropertyOriginTests { @Test public void equalsAndHashCodeShouldResourceAndLocation() throws Exception { - TextResourcePropertyOrigin origin1 = new TextResourcePropertyOrigin( + TextResourceOrigin origin1 = new TextResourceOrigin( new ClassPathResource("foo.txt"), new Location(1, 2)); - TextResourcePropertyOrigin origin2 = new TextResourcePropertyOrigin( + TextResourceOrigin origin2 = new TextResourceOrigin( new ClassPathResource("foo.txt"), new Location(1, 2)); - TextResourcePropertyOrigin origin3 = new TextResourcePropertyOrigin( + TextResourceOrigin origin3 = new TextResourceOrigin( new ClassPathResource("foo.txt"), new Location(2, 2)); - TextResourcePropertyOrigin origin4 = new TextResourcePropertyOrigin( + TextResourceOrigin origin4 = new TextResourceOrigin( new ClassPathResource("foo2.txt"), new Location(1, 2)); assertThat(origin1.hashCode()).isEqualTo(origin1.hashCode()); assertThat(origin1.hashCode()).isEqualTo(origin2.hashCode()); From a5651b75de0af1b28337a4fc6ccfd1c543688f11 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 17 Apr 2017 11:42:49 -0700 Subject: [PATCH 03/19] Create ConfigurationPropertySource abstraction Add a new abstraction that represents a source for configuration properties. The new source is similar to the `Environment` abstraction provided by Spring Framework but follows a stricter set of rules. The `ConfigurationPropertySource` provides a uniform view onto a source and will help to move responsibility for accessing properties in a "relaxed" way away from the caller. The `ConfigurationPropertyName` class enforces strict naming rules that callers must follow when accessing properties. Configuration names are lowercase, dot separated and may contain dashes. In addition "indexed" name elements may be defined by using square brackets. Mapping is provided to existing Spring PropertySources implemented with the relaxed rules that users are used to. For example the configuration property `server.local-host` can still be written in a property files as `server.localHost` or in an environment variable as SERVER_LOCALHOST. Closes gh-4910 --- .../boot/context/properties/package-info.java | 5 +- .../AliasedConfigurationPropertySource.java | 73 +++ .../source/ConfigurationProperty.java | 115 +++++ .../source/ConfigurationPropertyName.java | 439 ++++++++++++++++++ .../ConfigurationPropertyNameAliases.java | 77 +++ .../ConfigurationPropertyNameBuilder.java | 249 ++++++++++ .../source/ConfigurationPropertySource.java | 87 ++++ .../source/ConfigurationPropertySources.java | 162 +++++++ ...gurationPropertySourcesPropertySource.java | 77 +++ .../source/DefaultPropertyMapper.java | 106 +++++ ...FilteredConfigurationPropertiesSource.java | 57 +++ .../MapConfigurationPropertySource.java | 96 ++++ .../properties/source/PropertyMapper.java | 62 +++ .../properties/source/PropertyMapping.java | 102 ++++ ...ertySourceConfigurationPropertySource.java | 265 +++++++++++ .../SystemEnvironmentPropertyMapper.java | 164 +++++++ .../properties/source/package-info.java | 22 + .../source/AbstractPropertyMapperTests.java | 58 +++ ...iasedConfigurationPropertySourceTests.java | 72 +++ ...ConfigurationPropertyNameAliasesTests.java | 124 +++++ ...ConfigurationPropertyNameBuilderTests.java | 138 ++++++ .../ConfigurationPropertyNameTests.java | 326 +++++++++++++ ...ionPropertySourcesPropertySourceTests.java | 96 ++++ .../ConfigurationPropertySourcesTests.java | 180 +++++++ .../source/ConfigurationPropertyTests.java | 97 ++++ .../source/DefaultPropertyMapperTests.java | 70 +++ ...redConfigurationPropertiesSourceTests.java | 89 ++++ .../MapConfigurationPropertySourceTests.java | 118 +++++ .../MockConfigurationPropertySource.java | 100 ++++ ...ourceConfigurationPropertySourceTests.java | 265 +++++++++++ .../SystemEnvironmentPropertyMapperTests.java | 131 ++++++ 31 files changed, 4019 insertions(+), 3 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySource.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationProperty.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliases.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySource.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSource.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapping.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySource.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/package-info.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySourceTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliasesTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySourceTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSourceTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySourceTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySourceTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/package-info.java index 6b1693d24c..9b32cf99cd 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/package-info.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -15,8 +15,7 @@ */ /** - * Support for external configuration binding via the {@code @ConfigurationProperties} - * annotation. + * Support for external configuration properties. * * @see org.springframework.boot.context.properties.ConfigurationProperties * @see org.springframework.boot.context.properties.EnableConfigurationProperties diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySource.java new file mode 100644 index 0000000000..acfb907db9 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySource.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.source; + +import java.util.List; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * A {@link ConfigurationPropertySource} supporting name aliases. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class AliasedConfigurationPropertySource implements ConfigurationPropertySource { + + private final ConfigurationPropertySource source; + + private final ConfigurationPropertyNameAliases aliases; + + AliasedConfigurationPropertySource(ConfigurationPropertySource source, + ConfigurationPropertyNameAliases aliases) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(aliases, "Aliases must not be null"); + this.source = source; + this.aliases = aliases; + } + + @Override + public Stream stream() { + return StreamSupport.stream(this.source.spliterator(), false) + .flatMap(this::addAliases); + } + + private Stream addAliases(ConfigurationPropertyName name) { + Stream names = Stream.of(name); + List aliases = this.aliases.getAliases(name); + if (CollectionUtils.isEmpty(aliases)) { + return names; + } + return Stream.concat(names, aliases.stream()); + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + Assert.notNull(name, "Name must not be null"); + ConfigurationProperty result = this.source.getConfigurationProperty(name); + if (result == null) { + ConfigurationPropertyName aliasedName = this.aliases.getNameForAlias(name); + result = this.source.getConfigurationProperty(aliasedName); + } + return result; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationProperty.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationProperty.java new file mode 100644 index 0000000000..30ded56ab3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationProperty.java @@ -0,0 +1,115 @@ +/* + * 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.source; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A single configuration property obtained from a {@link ConfigurationPropertySource} + * consisting of a {@link #getName() name}, {@link #getValue() value} and optional + * {@link #getOrigin() origin}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public final class ConfigurationProperty + implements OriginProvider, Comparable { + + private final ConfigurationPropertyName name; + + private final Object value; + + private final Origin origin; + + public ConfigurationProperty(ConfigurationPropertyName name, Object value, + Origin origin) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(value, "Value must not be null"); + this.name = name; + this.value = value; + this.origin = origin; + } + + public ConfigurationPropertyName getName() { + return this.name; + } + + public Object getValue() { + return this.value; + } + + @Override + public Origin getOrigin() { + return this.origin; + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(this.name); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.value); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ConfigurationProperty other = (ConfigurationProperty) obj; + boolean result = true; + result = result && ObjectUtils.nullSafeEquals(this.name, other.name); + result = result && ObjectUtils.nullSafeEquals(this.value, other.value); + return result; + } + + @Override + public String toString() { + return new ToStringCreator(this).append("name", this.name) + .append("value", this.value).append("origin", this.origin).toString(); + } + + @Override + public int compareTo(ConfigurationProperty other) { + return this.name.compareTo(other.name); + } + + static ConfigurationProperty of(ConfigurationPropertyName name, + OriginTrackedValue value) { + if (value == null) { + return null; + } + return new ConfigurationProperty(name, value.getValue(), value.getOrigin()); + } + + static ConfigurationProperty of(ConfigurationPropertyName name, Object value, + Origin origin) { + if (value == null) { + return null; + } + return new ConfigurationProperty(name, value, origin); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java new file mode 100644 index 0000000000..1680b6a616 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java @@ -0,0 +1,439 @@ +/* + * 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.source; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Element; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * A configuration property name composed of elements separated by dots. Names may contain + * the characters ("{@code a-z}" "{@code 0-9}") & "{@code -}", they must be lower-case and + * must start with a letter. The "{@code -}" is used purely for formatting, i.e. + * "{@code foo-bar}" and "{@code foobar}" are considered equivalent. + *

+ * The "{@code [}" and "{@code ]}" characters may be used to indicate an associative + * index(i.e. a {@link Map} key or a {@link Collection} index. Indexes names are not + * restricted and are considered case-sensitive. + *

+ * Here are some typical examples: + *

    + *
  • {@code spring.main.banner-mode}
  • + *
  • {@code server.hosts[0].name}
  • + *
  • {@code log[org.springboot].level}
  • + *
+ *

+ * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see #of(String) + * @see ConfigurationPropertyNameBuilder + * @see ConfigurationPropertySource + */ +public final class ConfigurationPropertyName + implements Iterable, Comparable { + + /** + * An empty {@link ConfigurationPropertyName}. + */ + public static final ConfigurationPropertyName EMPTY = new ConfigurationPropertyName( + null, new Element()); + + private static final ConfigurationPropertyNameBuilder BUILDER = new ConfigurationPropertyNameBuilder( + Pattern.compile("[a-z]([a-z0-9\\-])*")); + + private final ConfigurationPropertyName parent; + + private final Element element; + + private String toString; + + ConfigurationPropertyName(ConfigurationPropertyName parent, Element element) { + Assert.notNull(element, "Element must not be null"); + this.parent = parent; + this.element = element; + } + + /** + * Return the parent of this configuration property. + * @return the parent or {code null} + */ + public ConfigurationPropertyName getParent() { + return this.parent; + } + + /** + * Return the element part of this configuration property name. + * @return the element (never {@code null}) + */ + public Element getElement() { + return this.element; + } + + @Override + public Iterator iterator() { + return stream().iterator(); + } + + /** + * Return a stream of the {@link Element Elements} that make up this name. + * @return a stream of {@link Element} items + */ + public Stream stream() { + if (this.parent == null) { + return Stream.of(this.element); + } + return Stream.concat(this.parent.stream(), Stream.of(this.element)); + } + + /** + * Return a stream of the {@link Element Elements} that make up this name starting + * from the given root. + * @param root the root of the name or {@code null} to stream all elements + * @return a stream of {@link Element} items + */ + public Stream stream(ConfigurationPropertyName root) { + if (this.parent == null || this.parent.equals(root)) { + return Stream.of(this.element); + } + return Stream.concat(this.parent.stream(root), Stream.of(this.element)); + } + + @Override + public String toString() { + if (this.toString == null) { + this.toString = buildToString(); + } + return this.toString; + } + + private String buildToString() { + StringBuilder result = new StringBuilder(); + result.append(this.parent != null ? this.parent.toString() : ""); + result.append(result.length() > 0 && !this.element.isIndexed() ? "." : ""); + result.append(this.element); + return result.toString(); + } + + /** + * Returns {@code true} if this element is an ancestor (immediate or nested parent) or + * the specified name. + * @param name the name to check + * @return {@code true} if this name is an ancessor + */ + public boolean isAncestorOf(ConfigurationPropertyName name) { + ConfigurationPropertyName candidate = (name == null ? null : name.getParent()); + while (candidate != null) { + if (candidate.equals(this)) { + return true; + } + candidate = candidate.getParent(); + } + return false; + } + + @Override + public int compareTo(ConfigurationPropertyName other) { + Iterator elements = iterator(); + Iterator otherElements = other.iterator(); + while (elements.hasNext() || otherElements.hasNext()) { + int result = compare(elements.hasNext() ? elements.next() : null, + otherElements.hasNext() ? otherElements.next() : null); + if (result != 0) { + return result; + } + } + return 0; + } + + private int compare(Element element, Element other) { + if (element == null) { + return -1; + } + if (other == null) { + return 1; + } + return element.compareTo(other); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + ObjectUtils.nullSafeHashCode(this.parent); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.element); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ConfigurationPropertyName other = (ConfigurationPropertyName) obj; + boolean result = true; + result = result && ObjectUtils.nullSafeEquals(this.parent, other.parent); + result = result && ObjectUtils.nullSafeEquals(this.element, other.element); + return result; + } + + /** + * Create a new {@link ConfigurationPropertyName} by appending the given index. + * @param index the index to append + * @return a new {@link ConfigurationPropertyName} + */ + public ConfigurationPropertyName appendIndex(int index) { + return append("[" + index + "]"); + } + + /** + * Create a new {@link ConfigurationPropertyName} by appending the given element. + * @param element the element to append + * @return a new {@link ConfigurationPropertyName} + */ + public ConfigurationPropertyName append(String element) { + if (StringUtils.hasLength(element)) { + return BUILDER.from(this).append(element).build(); + } + return this; + } + + /** + * Return a {@link ConfigurationPropertyName} for the specified string. + * @param name the source name + * @return a {@link ConfigurationPropertyName} instance + * @throws IllegalArgumentException if the name is not valid + */ + public static ConfigurationPropertyName of(String name) + throws IllegalArgumentException { + Assert.notNull(name, "Name must not be null"); + Assert.isTrue(!name.toString().startsWith("."), "Name must not start with '.'"); + Assert.isTrue(!name.toString().endsWith("."), "Name must not end with '.'"); + if (StringUtils.isEmpty(name)) { + return EMPTY; + } + return BUILDER.from(name, '.').build(); + } + + /** + * An individual element of the {@link ConfigurationPropertyName}. + */ + public static final class Element implements Comparable { + + private static final Pattern VALUE_PATTERN = Pattern.compile("[\\w\\-]+"); + + private final boolean indexed; + + private final String[] value; + + private Element() { + this.indexed = false; + this.value = Form.expand("", false); + } + + Element(String value) { + Assert.notNull(value, "Value must not be null"); + this.indexed = (value.startsWith("[") && value.endsWith("]")); + value = (this.indexed ? value.substring(1, value.length() - 1) : value); + if (!this.indexed) { + validate(value); + } + this.value = Form.expand(value, this.indexed); + } + + private void validate(String value) { + Assert.isTrue(VALUE_PATTERN.matcher(value).matches(), + "Element value '" + value + "' is not valid"); + } + + @Override + public int compareTo(Element other) { + int result = Boolean.compare(other.indexed, this.indexed); + if (result != 0) { + return result; + } + if (this.indexed && other.indexed) { + try { + long value = Long.parseLong(getValue(Form.UNIFORM)); + long otherValue = Long.parseLong(other.getValue(Form.UNIFORM)); + return Long.compare(value, otherValue); + } + catch (NumberFormatException ex) { + // Fallback to string comparison + } + } + return getValue(Form.UNIFORM).compareTo(other.getValue(Form.UNIFORM)); + } + + @Override + public int hashCode() { + return getValue(Form.UNIFORM).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return ObjectUtils.nullSafeEquals(getValue(Form.UNIFORM), + ((Element) obj).getValue(Form.UNIFORM)); + } + + @Override + public String toString() { + String string = getValue(Form.CONFIGURATION).toString(); + return (this.indexed ? "[" + string + "]" : string); + } + + /** + * Return if the element is indexed (i.e. should be displayed in angle brackets). + * @return if the element is indexed + */ + public boolean isIndexed() { + return this.indexed; + } + + /** + * Return the element value in the specified form. Indexed values (the part within + * square brackets) are always returned unchanged. + * @param form the form the value should take + * @return the value + */ + public String getValue(Form form) { + form = (form != null ? form : Form.ORIGINAL); + return this.value[form.ordinal()]; + } + + } + + /** + * The various forms that a non-indexed {@link Element} {@code value} can take. + */ + public enum Form { + + /** + * The original form as specified when the name was created. For example: + *

    + *
  • "{@code foo-bar}" = "{@code foo-bar}"
  • + *
  • "{@code fooBar}" = "{@code fooBar}"
  • + *
  • "{@code foo_bar}" = "{@code foo_bar}"
  • + *
  • "{@code [Foo.bar]}" = "{@code Foo.bar}"
  • + *
+ */ + ORIGINAL { + + @Override + protected String convert(String value) { + return value; + } + + }, + + /** + * The canonical configuration form (lower-case with only alphanumeric and + * "{@code -}" characters). + *
    + *
  • "{@code foo-bar}" = "{@code foo-bar}"
  • + *
  • "{@code fooBar}" = "{@code foobar}"
  • + *
  • "{@code foo_bar}" = "{@code foobar}"
  • + *
  • "{@code [Foo.bar]}" = "{@code Foo.bar}"
  • + *
+ */ + CONFIGURATION { + + @Override + protected boolean isIncluded(char ch) { + return Character.isAlphabetic(ch) || Character.isDigit(ch) || (ch == '-'); + } + + }, + + /** + * The uniform configuration form (used for equals/hashcode; lower-case with only + * alphanumeric characters). + *
    + *
  • "{@code foo-bar}" = "{@code foobar}"
  • + *
  • "{@code fooBar}" = "{@code foobar}"
  • + *
  • "{@code foo_bar}" = "{@code foobar}"
  • + *
  • "{@code [Foo.bar]}" = "{@code Foo.bar}"
  • + *
+ */ + UNIFORM { + + @Override + protected boolean isIncluded(char ch) { + return Character.isAlphabetic(ch) || Character.isDigit(ch); + } + + }; + + /** + * Called to convert an original value into the instance form. + * @param value the value to convert + * @return the converted value + */ + protected String convert(String value) { + StringBuilder result = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + if (isIncluded(ch)) { + result.append(Character.toLowerCase(ch)); + } + } + return result.toString(); + } + + /** + * Called to determine of the specified character is valid for the form. + * @param ch the character to test + * @return if the character is value + */ + protected boolean isIncluded(char ch) { + return true; + } + + /** + * Expand the given value to all an array containing the value in each + * {@link Form}. + * @param value the source value + * @param indexed if the value is indexed + * @return an array of all forms (in the same order as {@link Form#values()}. + */ + protected static String[] expand(String value, boolean indexed) { + String[] result = new String[values().length]; + for (Form form : values()) { + result[form.ordinal()] = (indexed ? value : form.convert(value)); + } + return result; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliases.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliases.java new file mode 100644 index 0000000000..022c124cdb --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliases.java @@ -0,0 +1,77 @@ +/* + * 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.source; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Maintains a mapping of {@link ConfigurationPropertyName} alaises. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see ConfigurationPropertySource#withAliases(ConfigurationPropertyNameAliases) + */ +public final class ConfigurationPropertyNameAliases { + + private final MultiValueMap aliases = new LinkedMultiValueMap<>(); + + public ConfigurationPropertyNameAliases() { + } + + public ConfigurationPropertyNameAliases(String name, String... aliases) { + addAlaises(name, aliases); + } + + public ConfigurationPropertyNameAliases(ConfigurationPropertyName name, + ConfigurationPropertyName... aliases) { + addAlaises(name, aliases); + } + + public void addAlaises(String name, String... aliases) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(aliases, "Aliases must not be null"); + addAlaises(ConfigurationPropertyName.of(name), + Arrays.stream(aliases).map(ConfigurationPropertyName::of) + .toArray(ConfigurationPropertyName[]::new)); + } + + public void addAlaises(ConfigurationPropertyName name, + ConfigurationPropertyName... aliases) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(aliases, "Aliases must not be null"); + this.aliases.addAll(name, Arrays.asList(aliases)); + } + + public List getAliases(ConfigurationPropertyName name) { + return this.aliases.getOrDefault(name, Collections.emptyList()); + } + + public ConfigurationPropertyName getNameForAlias(ConfigurationPropertyName alias) { + return this.aliases.entrySet().stream() + .filter((e) -> e.getValue().contains(alias)).map(Map.Entry::getKey) + .findFirst().orElse(null); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilder.java new file mode 100644 index 0000000000..2738fc840d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilder.java @@ -0,0 +1,249 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.source; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Element; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Builder class that can be used to create {@link ConfigurationPropertyName + * ConfigurationPropertyNames}. This class is intended for use within custom + * {@link ConfigurationPropertySource} implementations. When accessing + * {@link ConfigurationProperty properties} from and existing + * {@link ConfigurationPropertySource source} the + * {@link ConfigurationPropertyName#of(String)} method should be used to obtain a + * {@link ConfigurationPropertyName name}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see ConfigurationPropertyName + */ +public class ConfigurationPropertyNameBuilder { + + private final ElementValueProcessor processor; + + private final List elements; + + /** + * Create a new {@link ConfigurationPropertyNameBuilder} instance. + */ + public ConfigurationPropertyNameBuilder() { + this(ElementValueProcessor.empty()); + } + + /** + * Create a new {@link ConfigurationPropertyNameBuilder} instance that enforces a + * {@link Pattern} on all element values. + * @param elementValuePattern the element value pattern to enforce + */ + public ConfigurationPropertyNameBuilder(Pattern elementValuePattern) { + this(ElementValueProcessor.empty().withPatternCheck(elementValuePattern)); + } + + /** + * Create a new {@link ConfigurationPropertyNameBuilder} with the specified + * {@link ElementValueProcessor}. + * @param processor the element value processor. + */ + public ConfigurationPropertyNameBuilder(ElementValueProcessor processor) { + Assert.notNull(processor, "Processor must not be null"); + this.processor = processor; + this.elements = Collections.emptyList(); + } + + /** + * Internal constructor used to create new builders. + * @param processor the element value processor. + * @param elements the elements built so far + */ + private ConfigurationPropertyNameBuilder(ElementValueProcessor processor, + List elements) { + this.processor = processor; + this.elements = elements; + } + + /** + * Start building using the specified name split up into elements using a known + * separator. For example {@code from("foo.bar", '.')} will return a new builder + * containing the elements "{@code foo}" and "{@code bar}". Any element in square + * brackets will be considered "indexed" and will not be considered for splitting. + * @param name the name build from + * @param separator the separator + * @return a builder with elements populated from the name + */ + public ConfigurationPropertyNameBuilder from(String name, char separator) { + Assert.notNull(name, "Name must not be null"); + List elements = new ArrayList<>(); + StringBuilder value = new StringBuilder(name.length()); + boolean indexed = false; + for (int i = 0; i < name.length(); i++) { + char ch = name.charAt(i); + if (!indexed) { + if (ch == '[') { + addElement(elements, value); + value.append(ch); + indexed = true; + } + else if (ch == separator) { + addElement(elements, value); + } + else { + value.append(ch); + } + } + else { + value.append(ch); + if (ch == ']') { + addElement(elements, value); + indexed = false; + } + } + } + addElement(elements, value); + return from(elements.stream().filter(Objects::nonNull) + .filter((e) -> !e.getValue(Form.UNIFORM).isEmpty()).iterator()); + } + + private void addElement(List elements, StringBuilder value) { + if (value.length() > 0) { + elements.add(buildElement(value.toString())); + value.setLength(0); + } + } + + /** + * Return a new {@link ConfigurationPropertyNameBuilder} starting with the specified + * elements. + * @param elements the elements that the new builder should contain + * @return a new initialized builder instance + */ + public ConfigurationPropertyNameBuilder from(Iterable elements) { + return from(elements.iterator()); + } + + /** + * Return a new {@link ConfigurationPropertyNameBuilder} starting with the specified + * elements. + * @param elements the elements that the new builder should contain + * @return a new initialized builder instance + */ + public ConfigurationPropertyNameBuilder from(Iterator elements) { + Assert.state(CollectionUtils.isEmpty(this.elements), + "Existing elements must not be present"); + return new ConfigurationPropertyNameBuilder(this.processor, toList(elements)); + } + + private List toList(Iterator iterator) { + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + if (isRoot(list)) { + return Collections.emptyList(); + } + return list; + } + + private boolean isRoot(List list) { + return (list.size() == 1 && list.get(0).getValue(Form.ORIGINAL).isEmpty()); + } + + /** + * Return a new builder containing the elements built so far appended with the + * specified element value. + * @param elementValue the element value to append + * @return a new builder instance + */ + public ConfigurationPropertyNameBuilder append(String elementValue) { + Assert.notNull(elementValue, "ElementValue must not be null"); + List elements = new ArrayList<>(this.elements); + elements.add(buildElement(elementValue)); + return new ConfigurationPropertyNameBuilder(this.processor, elements); + } + + private Element buildElement(String value) { + return new Element(this.processor.apply(value)); + } + + /** + * Build a new {@link ConfigurationPropertyName} from the elements contained in this + * builder. + * @return a new {@link ConfigurationPropertyName}. + */ + public ConfigurationPropertyName build() { + ConfigurationPropertyName name = null; + for (Element element : this.elements) { + name = new ConfigurationPropertyName(name, element); + } + Assert.state(name != null, "At least one element must be defined"); + return name; + } + + /** + * An processor that will be applied to element values. Can be used to manipulate or + * restrict the values that are used. + */ + @FunctionalInterface + public interface ElementValueProcessor { + + /** + * Apply the processor to the specified value. + * @param value the value to process + * @return the processed value + * @throws RuntimeException if the value cannot be used + */ + String apply(String value) throws RuntimeException; + + /** + * Extend this processor with a {@link Pattern} regular expression check. + * @param pattern the patter to check + * @return an element processor that additionally checks against the pattern + */ + default ElementValueProcessor withPatternCheck(Pattern pattern) { + Assert.notNull(pattern, "Pattern must not be null"); + return (value) -> { + value = apply(value); + Element element = new Element(value); + Assert.isTrue(element.isIndexed() || pattern.matcher(value).matches(), + "Element value '" + value + "' is not valid (" + pattern + + " does not match)"); + return value; + }; + } + + /** + * Return an empty {@link ElementValueProcessor} that simply returns the original + * value unchanged. + * @return an empty {@link ElementValueProcessor}. + */ + static ElementValueProcessor empty() { + return (value) -> value; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySource.java new file mode 100644 index 0000000000..4f75405787 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySource.java @@ -0,0 +1,87 @@ +/* + * 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.source; + +import java.util.Iterator; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.core.env.PropertySource; + +/** + * A source of {@link ConfigurationProperty ConfigurationProperties}, usually backed by a + * Spring {@link PropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see ConfigurationPropertyName + * @see OriginTrackedValue + * @see #getConfigurationProperty(ConfigurationPropertyName) + */ +public interface ConfigurationPropertySource extends Iterable { + + /** + * Return a single {@link ConfigurationProperty} from the source or {@code null} if no + * property can be found. + * @param name the name of the property (must not be {@code null}) + * @return the associated object or {@code null}. + */ + ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name); + + /** + * Return an iterator for the {@link ConfigurationPropertyName names} managed by this + * source. If it is not possible to determine the names an empty iterator may be + * returned. + * @return an iterator (never {@code null}) + */ + @Override + default Iterator iterator() { + return stream().iterator(); + } + + /** + * Returns a sequential {@code Stream} for the {@link ConfigurationPropertyName names} + * managed by this source. If it is not possible to determine the names an + * {@link Stream#empty() empty stream} may be returned. + * @return a stream of names (never {@code null}) + */ + Stream stream(); + + /** + * Return a filtered variant of this source, containing only names that match the + * given {@link Predicate}. + * @param filter the filter to apply + * @return a filtered {@link ConfigurationPropertySource} instance + */ + default ConfigurationPropertySource filter( + Predicate filter) { + return new FilteredConfigurationPropertiesSource(this, filter); + } + + /** + * Return a variant of this source that supports name aliases. + * @param aliases a function that returns a stream of aliases for any given name + * @return a {@link ConfigurationPropertySource} instance supporting name alaises + */ + default ConfigurationPropertySource withAliases( + ConfigurationPropertyNameAliases aliases) { + return new AliasedConfigurationPropertySource(this, aliases); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java new file mode 100644 index 0000000000..be7fae3f46 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.source; + +import java.util.Iterator; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySources; +import org.springframework.core.env.PropertySourcesPropertyResolver; +import org.springframework.core.env.SystemEnvironmentPropertySource; +import org.springframework.util.Assert; + +/** + * A managed set of {@link ConfigurationPropertySource} instances, usually adapted from + * Spring's {@link PropertySources}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see #attach(MutablePropertySources) + * @see #get(PropertySources) + */ +public class ConfigurationPropertySources + implements Iterable { + + /** + * The name of the {@link PropertySource} {@link #adapt adapter}. + */ + public static final String PROPERTY_SOURCE_NAME = "configurationPropertes"; + + private final PropertySources propertySources; + + private final Map, ConfigurationPropertySource> adapters = new WeakHashMap<>(); + + /** + * Create a new {@link ConfigurationPropertySources} instance. + * @param propertySources the property sources to expose + */ + ConfigurationPropertySources(PropertySources propertySources) { + Assert.notNull(propertySources, "PropertySources must not be null"); + this.propertySources = propertySources; + } + + @Override + public Iterator iterator() { + return streamPropertySources(this.propertySources) + .filter(s -> !(s instanceof ConfigurationPropertySourcesPropertySource)) + .map(this::adapt).collect(Collectors.toList()).iterator(); + } + + private Stream> streamPropertySources(PropertySources sources) { + return StreamSupport.stream(sources.spliterator(), false).flatMap(this::flatten); + } + + private Stream> flatten(PropertySource source) { + if (source.getSource() instanceof ConfigurableEnvironment) { + return streamPropertySources( + ((ConfigurableEnvironment) source.getSource()).getPropertySources()); + } + return Stream.of(source); + } + + private ConfigurationPropertySource adapt(PropertySource source) { + return this.adapters.computeIfAbsent(source, (k) -> { + return new PropertySourceConfigurationPropertySource(source, + getPropertyMapper(source)); + }); + } + + private PropertyMapper getPropertyMapper(PropertySource source) { + if (source instanceof SystemEnvironmentPropertySource) { + return SystemEnvironmentPropertyMapper.INSTANCE; + } + return DefaultPropertyMapper.INSTANCE; + } + + /** + * Attach a {@link ConfigurationPropertySources} instance to the specified + * {@link ConfigurableEnvironment} so that classic + * {@link PropertySourcesPropertyResolver} calls will resolve using + * {@link ConfigurationPropertyName configuration property names}. + * @param environment the source environment + * @return the instance attached + */ + public static ConfigurationPropertySources attach( + ConfigurableEnvironment environment) { + return attach(environment.getPropertySources()); + } + + /** + * Attach a {@link ConfigurationPropertySources} instance to the specified + * {@link PropertySources} so that classic {@link PropertySourcesPropertyResolver} + * calls will resolve using using {@link ConfigurationPropertyName configuration + * property names}. + * @param propertySources the source property sources + * @return the instance attached + */ + public static ConfigurationPropertySources attach( + MutablePropertySources propertySources) { + ConfigurationPropertySources adapted = new ConfigurationPropertySources( + propertySources); + propertySources.addFirst(new ConfigurationPropertySourcesPropertySource( + PROPERTY_SOURCE_NAME, adapted)); + return adapted; + } + + /** + * Get a {@link ConfigurationPropertySources} instance for the specified + * {@link PropertySources} (either previously {@link #attach(MutablePropertySources) + * attached} or a new instance. + * @param propertySources the source property sources + * @return a {@link ConfigurationPropertySources} instance + */ + public static ConfigurationPropertySources get(PropertySources propertySources) { + if (propertySources == null) { + return null; + } + PropertySource source = propertySources.get(PROPERTY_SOURCE_NAME); + if (source != null) { + return (ConfigurationPropertySources) source.getSource(); + } + return new ConfigurationPropertySources(propertySources); + } + + /** + * Get a {@link ConfigurationPropertySources} instance for the {@link PropertySources} + * from the specified {@link ConfigurableEnvironment}, (either previously + * {@link #attach(MutablePropertySources) attached} or a new instance. + * @param environment the configurable environment + * @return a {@link ConfigurationPropertySources} instance + */ + public static ConfigurationPropertySources get(ConfigurableEnvironment environment) { + MutablePropertySources propertySources = environment.getPropertySources(); + PropertySource source = propertySources.get(PROPERTY_SOURCE_NAME); + if (source != null) { + return (ConfigurationPropertySources) source.getSource(); + } + return new ConfigurationPropertySources(propertySources); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java new file mode 100644 index 0000000000..eff2cb2469 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java @@ -0,0 +1,77 @@ +/* + * 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.source; + +import java.util.Objects; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.PropertySource; + +/** + * {@link PropertySource} that exposes {@link ConfigurationPropertySource} instances so + * that they can be used with a {@link PropertyResolver} or added to the + * {@link Environment}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigurationPropertySourcesPropertySource + extends PropertySource> + implements OriginLookup { + + ConfigurationPropertySourcesPropertySource(String name, + Iterable source) { + super(name, source); + } + + @Override + public Object getProperty(String name) { + ConfigurationProperty configurationProperty = findConfigurationProperty(name); + return (configurationProperty == null ? null : configurationProperty.getValue()); + } + + @Override + public Origin getOrigin(String name) { + return Origin.from(findConfigurationProperty(name)); + } + + private ConfigurationProperty findConfigurationProperty(String name) { + try { + return findConfigurationProperty(ConfigurationPropertyName.of(name)); + } + catch (Exception ex) { + return null; + } + } + + private ConfigurationProperty findConfigurationProperty( + ConfigurationPropertyName name) { + if (name == null) { + return null; + } + Stream sources = StreamSupport + .stream(getSource().spliterator(), false); + return sources.map(source -> source.getConfigurationProperty(name)) + .filter(Objects::nonNull).findFirst().orElse(null); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java new file mode 100644 index 0000000000..0718e69c34 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.source; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.core.env.PropertySource; + +/** + * Default {@link PropertyMapper} implementation. Names are mapped by removing invalid + * characters and converting to lower case. For example "{@code my.server_name.PORT}" is + * mapped to "{@code my.servername.port}". + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertyMapper + * @see PropertySourceConfigurationPropertySource + */ +class DefaultPropertyMapper implements PropertyMapper { + + public static final PropertyMapper INSTANCE = new DefaultPropertyMapper(); + + private Cache configurationPropertySourceCache = new Cache<>(); + + private Cache propertySourceCache = new Cache<>(); + + private final ConfigurationPropertyNameBuilder nameBuilder = new ConfigurationPropertyNameBuilder(); + + @Override + public List map(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName) { + List mapping = this.configurationPropertySourceCache + .get(configurationPropertyName); + if (mapping == null) { + String convertedName = configurationPropertyName.toString(); + mapping = Collections.singletonList( + new PropertyMapping(convertedName, configurationPropertyName)); + } + return mapping; + } + + @Override + public List map(PropertySource propertySource, + String propertySourceName) { + List mapping = this.propertySourceCache.get(propertySourceName); + if (mapping == null) { + mapping = tryMap(propertySourceName); + this.propertySourceCache.put(propertySourceName, mapping); + } + return mapping; + } + + private List tryMap(String propertySourceName) { + try { + ConfigurationPropertyName convertedName = this.nameBuilder + .from(propertySourceName, '.').build(); + PropertyMapping o = new PropertyMapping(propertySourceName, convertedName); + return Collections.singletonList(o); + } + catch (Exception ex) { + return Collections.emptyList(); + } + } + + private static class Cache extends LinkedHashMap> { + + private final int capacity; + + Cache() { + this(1); + } + + Cache(int capacity) { + super(capacity, (float) 0.75, true); + this.capacity = capacity; + } + + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + if (size() < this.capacity) { + return false; + } + return true; + + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSource.java new file mode 100644 index 0000000000..407bec986c --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSource.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.source; + +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.util.Assert; + +/** + * A filtered {@link ConfigurationPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class FilteredConfigurationPropertiesSource implements ConfigurationPropertySource { + + private final ConfigurationPropertySource source; + + private final Predicate filter; + + FilteredConfigurationPropertiesSource(ConfigurationPropertySource source, + Predicate filter) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(filter, "Filter must not be null"); + this.source = source; + this.filter = filter; + } + + @Override + public Stream stream() { + return StreamSupport.stream(this.source.spliterator(), false).filter(this.filter); + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + return (this.filter.test(name) ? this.source.getConfigurationProperty(name) + : null); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java new file mode 100644 index 0000000000..d659e540f2 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.source; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.util.Assert; + +/** + * An {@link ConfigurationPropertySource} backed by a {@link Map} and using standard name + * mapping rules. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class MapConfigurationPropertySource implements ConfigurationPropertySource { + + private final Map source; + + private final ConfigurationPropertySource delegate; + + /** + * Create a new empty {@link MapConfigurationPropertySource} instance. + */ + public MapConfigurationPropertySource() { + this(Collections.emptyMap()); + } + + /** + * Create a new {@link MapConfigurationPropertySource} instance with entries copies + * from the specified map. + * @param map the source map + */ + public MapConfigurationPropertySource(Map map) { + this.source = new LinkedHashMap<>(); + this.delegate = new PropertySourceConfigurationPropertySource( + new MapPropertySource("source", this.source), + new DefaultPropertyMapper()); + putAll(map); + } + + /** + * Add all enties from the specified map. + * @param map the source map + */ + public void putAll(Map map) { + Assert.notNull(map, "Map must not be null"); + map.forEach(this::put); + } + + /** + * Add an individual entry. + * @param name the name + * @param value the value + */ + public void put(Object name, Object value) { + this.source.put((name == null ? null : name.toString()), value); + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + return this.delegate.getConfigurationProperty(name); + } + + @Override + public Iterator iterator() { + return this.delegate.iterator(); + } + + @Override + public Stream stream() { + return this.delegate.stream(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java new file mode 100644 index 0000000000..c1d5d8164e --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.source; + +import java.util.List; + +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; + +/** + * Strategy used to provide a mapping between a {@link PropertySource} and a + * {@link ConfigurationPropertySource}. + *

+ * Mappings should be provided for both {@link ConfigurationPropertyName + * ConfigurationPropertyName} types and {@code String} based names. This allows the + * {@link PropertySourceConfigurationPropertySource} to first attempt any direct mappings + * (i.e. map the {@link ConfigurationPropertyName} directly to the {@link PropertySource} + * name) before falling back to {@link EnumerablePropertySource enumerating} property + * names, mapping them to a {@link ConfigurationPropertyName} and checking for + * {@link PropertyMapping#isApplicable(ConfigurationPropertyName) applicability}. See + * {@link PropertySourceConfigurationPropertySource} for more details. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertySourceConfigurationPropertySource + */ +interface PropertyMapper { + + /** + * Provide mappings from a {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName}. + * @param propertySource the property source + * @param configurationPropertyName the name to map + * @return a stream of mappings or {@code Stream#empty()} + */ + List map(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName); + + /** + * Provide mappings from a {@link PropertySource} property name. + * @param propertySource the property source + * @param propertySourceName the name to map + * @return a stream of mappings or {@code Stream#empty()} + */ + List map(PropertySource propertySource, + String propertySourceName); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapping.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapping.java new file mode 100644 index 0000000000..be60439282 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapping.java @@ -0,0 +1,102 @@ +/* + * 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.source; + +import java.util.function.Function; + +import org.springframework.core.env.PropertySource; + +/** + * Details a mapping between a {@link PropertySource} item and a + * {@link ConfigurationPropertySource} item. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertySourceConfigurationPropertySource + */ +class PropertyMapping { + + private final String propertySourceName; + + private final ConfigurationPropertyName configurationPropertyName; + + private final Function valueExtractor; + + /** + * Create a new {@link PropertyMapper} instance. + * @param propertySourceName the {@link PropertySource} name + * @param configurationPropertyName the {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName} + */ + PropertyMapping(String propertySourceName, + ConfigurationPropertyName configurationPropertyName) { + this(propertySourceName, configurationPropertyName, Function.identity()); + } + + /** + * Create a new {@link PropertyMapper} instance. + * @param propertySourceName the {@link PropertySource} name + * @param configurationPropertyName the {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName} + * @param valueExtractor the extractor used to obtain the value + */ + PropertyMapping(String propertySourceName, + ConfigurationPropertyName configurationPropertyName, + Function valueExtractor) { + this.propertySourceName = propertySourceName; + this.configurationPropertyName = configurationPropertyName; + this.valueExtractor = valueExtractor; + } + + /** + * Return the mapped {@link PropertySource} name. + * @return the property source name (never {@code null}) + */ + public String getPropertySourceName() { + return this.propertySourceName; + + } + + /** + * Return the mapped {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName}. + * @return the configuration property source name (never {@code null}) + */ + public ConfigurationPropertyName getConfigurationPropertyName() { + return this.configurationPropertyName; + + } + + /** + * Return a function that can be used to extract the {@link PropertySource} value. + * @return the value extractor (never {@code null}) + */ + public Function getValueExtractor() { + return this.valueExtractor; + } + + /** + * Return if this mapping is applicable for the given + * {@link ConfigurationPropertyName}. + * @param name the name to check + * @return if the mapping is applicable + */ + public boolean isApplicable(ConfigurationPropertyName name) { + return this.configurationPropertyName.equals(name); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySource.java new file mode 100644 index 0000000000..696ea9668f --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySource.java @@ -0,0 +1,265 @@ +/* + * 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.source; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.PropertySourceOrigin; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link ConfigurationPropertySource} backed by a Spring {@link PropertySource}. Provides + * support for {@link EnumerablePropertySource} when possible but can also be used to + * non-enumerable property sources or restricted {@link EnumerablePropertySource} + * implementation (such as a security restricted {@code systemEnvironment} source). A + * {@link PropertySource} is adapted with the help of a {@link PropertyMapper} which + * provides the mapping rules for individual properties. + *

+ * Each + * {@link ConfigurationPropertySource#getConfigurationProperty(ConfigurationPropertyName) + * getValue} call initially attempts to + * {@link PropertyMapper#map(PropertySource, ConfigurationPropertyName) map} the + * {@link ConfigurationPropertyName} to one or more {@code String} based names. This + * allows fast property resolution for well formed property sources and allows the adapter + * to work with non {@link EnumerablePropertySource enumerable property sources}. + *

+ * If direct {@link ConfigurationPropertyName} to {@code String} mapping is unsuccessful a + * brute force approach is taken by {@link EnumerablePropertySource#getPropertyNames() + * enumerating} known {@code String} {@link PropertySource} names, mapping them to one or + * more {@link ConfigurationPropertyName} and checking for + * {@link PropertyMapping#isApplicable(ConfigurationPropertyName) applicability}. The + * enumeration approach supports property sources where it isn't practical to guess all + * direct mapping combinations. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertyMapper + */ +class PropertySourceConfigurationPropertySource implements ConfigurationPropertySource { + + private final PropertySource propertySource; + + private final PropertyMapper mapper; + + private volatile Object cacheKey; + + private volatile Cache cache; + + /** + * Create a new {@link PropertySourceConfigurationPropertySource} implementation. + * @param propertySource the source property source + * @param mapper the property mapper + */ + PropertySourceConfigurationPropertySource(PropertySource propertySource, + PropertyMapper mapper) { + Assert.notNull(propertySource, "PropertySource must not be null"); + Assert.notNull(mapper, "Mapper must not be null"); + this.propertySource = propertySource; + this.mapper = new ExceptionSwallowingPropertyMapper(mapper); + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + ConfigurationProperty configurationProperty = findDirectly(name); + if (configurationProperty == null) { + configurationProperty = findByEnumeration(name); + } + return configurationProperty; + } + + private ConfigurationProperty findDirectly(ConfigurationPropertyName name) { + List mappings = this.mapper.map(this.propertySource, name); + return find(mappings, name); + } + + private ConfigurationProperty findByEnumeration(ConfigurationPropertyName name) { + List mappings = getPropertyMappings(); + return find(mappings, name); + } + + private ConfigurationProperty find(List mappings, + ConfigurationPropertyName name) { + // Use for-loops rather than streams since this method is called often + for (PropertyMapping mapping : mappings) { + if (mapping.isApplicable(name)) { + ConfigurationProperty property = find(mapping); + if (property != null) { + return property; + } + } + } + return null; + } + + private ConfigurationProperty find(PropertyMapping mapping) { + String propertySourceName = mapping.getPropertySourceName(); + Object value = this.propertySource.getProperty(propertySourceName); + if (value == null) { + return null; + } + value = mapping.getValueExtractor().apply(value); + ConfigurationPropertyName configurationPropertyName = mapping + .getConfigurationPropertyName(); + Origin origin = PropertySourceOrigin.get(this.propertySource, propertySourceName); + return ConfigurationProperty.of(configurationPropertyName, value, origin); + } + + @Override + public Stream stream() { + return getConfigurationPropertyNames().stream(); + } + + @Override + public Iterator iterator() { + return getConfigurationPropertyNames().iterator(); + } + + private List getConfigurationPropertyNames() { + Cache cache = getCache(); + List names = (cache != null ? cache.getNames() : null); + if (names != null) { + return names; + } + List mappings = getPropertyMappings(); + names = new ArrayList(mappings.size()); + for (PropertyMapping mapping : mappings) { + names.add(mapping.getConfigurationPropertyName()); + } + names = Collections.unmodifiableList(names); + if (cache != null) { + cache.setNames(names); + } + return names; + } + + private List getPropertyMappings() { + if (!(this.propertySource instanceof EnumerablePropertySource)) { + return Collections.emptyList(); + } + Cache cache = getCache(); + List mappings = (cache != null ? cache.getMappings() : null); + if (mappings != null) { + return mappings; + } + String[] names = ((EnumerablePropertySource) this.propertySource) + .getPropertyNames(); + mappings = new ArrayList(names.length); + for (String name : names) { + mappings.addAll(this.mapper.map(this.propertySource, name)); + } + mappings = Collections.unmodifiableList(mappings); + if (cache != null) { + cache.setMappings(mappings); + } + return mappings; + } + + private Cache getCache() { + Object cacheKey = getCacheKey(); + if (cacheKey == null) { + return null; + } + if (ObjectUtils.nullSafeEquals(cacheKey, this.cacheKey)) { + return this.cache; + } + this.cache = new Cache(); + this.cacheKey = cacheKey; + return this.cache; + } + + private Object getCacheKey() { + if (this.propertySource instanceof MapPropertySource) { + return ((MapPropertySource) this.propertySource).getSource().keySet(); + } + if (this.propertySource instanceof EnumerablePropertySource) { + return ((EnumerablePropertySource) this.propertySource).getPropertyNames(); + } + return null; + } + + /** + * {@link PropertyMapper} that swallows exceptions when the mapping fails. + */ + private static class ExceptionSwallowingPropertyMapper implements PropertyMapper { + + private final PropertyMapper mapper; + + ExceptionSwallowingPropertyMapper(PropertyMapper mapper) { + this.mapper = mapper; + } + + @Override + public List map(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName) { + try { + return this.mapper.map(propertySource, configurationPropertyName); + } + catch (Exception ex) { + return Collections.emptyList(); + } + } + + @Override + public List map(PropertySource propertySource, + String propertySourceName) { + try { + return this.mapper.map(propertySource, propertySourceName); + } + catch (Exception ex) { + return Collections.emptyList(); + } + } + + } + + private static class Cache { + + private ConfigurationPropertyName knownMissingName; + + private List names; + + private List mappings; + + public List getNames() { + return this.names; + } + + public void setNames(List names) { + this.names = names; + } + + public List getMappings() { + return this.mappings; + } + + public void setMappings(List mappings) { + this.mappings = mappings; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java new file mode 100644 index 0000000000..c95e56ecca --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java @@ -0,0 +1,164 @@ +/* + * 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.source; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; +import org.springframework.core.env.PropertySource; +import org.springframework.util.StringUtils; + +/** + * {@link PropertyMapper} for system environment variables. Names are mapped by removing + * invalid characters, converting to lower case and replacing "{@code _}" with + * "{@code .}". For example, "{@code SERVER_PORT}" is mapped to "{@code server.port}". In + * addition, numeric elements are mapped to indexes (e.g. "{@code HOST_0}" is mapped to + * "{@code host[0]}"). + *

+ * List shortcuts (names that end with double underscore) are also supported by this + * mapper. For example, "{@code MY_LIST__=a,b,c}" is mapped to "{@code my.list[0]=a}", + * "{@code my.list[1]=b}" ,"{@code my.list[2]=c}". + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertyMapper + * @see PropertySourceConfigurationPropertySource + */ +class SystemEnvironmentPropertyMapper implements PropertyMapper { + + public static PropertyMapper INSTANCE = new SystemEnvironmentPropertyMapper(); + + private final ConfigurationPropertyNameBuilder nameBuilder = new ConfigurationPropertyNameBuilder( + this::createElement); + + @Override + public List map(PropertySource propertySource, + String propertySourceName) { + ConfigurationPropertyName name = convertName(propertySourceName); + if (name == null) { + return Collections.emptyList(); + } + if (propertySourceName.endsWith("__")) { + return expandListShortcut(propertySourceName, name, + propertySource.getProperty(propertySourceName)); + } + return Collections.singletonList(new PropertyMapping(propertySourceName, name)); + } + + private ConfigurationPropertyName convertName(String propertySourceName) { + try { + return this.nameBuilder.from(propertySourceName, '_').build(); + } + catch (Exception ex) { + return null; + } + } + + private List expandListShortcut(String propertySourceName, + ConfigurationPropertyName rootName, Object value) { + if (value == null) { + return Collections.emptyList(); + } + List mappings = new ArrayList<>(); + String[] elements = StringUtils + .commaDelimitedListToStringArray(String.valueOf(value)); + for (int i = 0; i < elements.length; i++) { + ConfigurationPropertyName name = ConfigurationPropertyName + .of(rootName.toString() + "[" + i + "]"); + mappings.add(new PropertyMapping(propertySourceName, name, + new ElementExtractor(i))); + } + return mappings; + } + + @Override + public List map(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName) { + String name = convertName(configurationPropertyName); + List result = Collections + .singletonList(new PropertyMapping(name, configurationPropertyName)); + if (isListShortcutPossible(configurationPropertyName)) { + result = new ArrayList<>(result); + result.addAll(mapListShortcut(propertySource, configurationPropertyName)); + } + return result; + } + + private String convertName(ConfigurationPropertyName configurationPropertyName) { + String propertyName = configurationPropertyName.stream() + .map(name -> name.getValue(Form.UNIFORM).toUpperCase()) + .collect(Collectors.joining("_")); + return propertyName; + } + + private boolean isListShortcutPossible(ConfigurationPropertyName name) { + return (name.getElement().isIndexed() + && isNumber(name.getElement().getValue(Form.UNIFORM)) + && name.getParent() != null); + } + + private List mapListShortcut(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName) { + String propertyName = convertName(configurationPropertyName.getParent()) + "__"; + if (propertySource.containsProperty(propertyName)) { + int index = Integer.parseInt( + configurationPropertyName.getElement().getValue(Form.UNIFORM)); + return Collections.singletonList(new PropertyMapping(propertyName, + configurationPropertyName, new ElementExtractor(index))); + } + return Collections.emptyList(); + } + + private String createElement(String value) { + value = value.toLowerCase(); + return (isNumber(value) ? "[" + value + "]" : value); + } + + private static boolean isNumber(String string) { + IntStream nonDigits = string.chars().filter((c) -> !Character.isDigit(c)); + boolean hasNonDigit = nonDigits.findFirst().isPresent(); + return !hasNonDigit; + } + + /** + * Function used to extract an element from a comma list. + */ + private static class ElementExtractor implements Function { + + private final int index; + + ElementExtractor(int index) { + this.index = index; + } + + @Override + public Object apply(Object value) { + if (value == null) { + return null; + } + return StringUtils + .commaDelimitedListToStringArray(value.toString())[this.index]; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/package-info.java new file mode 100644 index 0000000000..4d0169c7d5 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * Sources for external configuration properties. + * + * @see org.springframework.boot.context.properties.source.ConfigurationPropertySource + */ +package org.springframework.boot.context.properties.source; diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java new file mode 100644 index 0000000000..2f4c8b72b1 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.source; + +import java.util.Collections; +import java.util.Iterator; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; + +/** + * Abstract base class for {@link PropertyMapper} tests. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public abstract class AbstractPropertyMapperTests { + + protected abstract PropertyMapper getMapper(); + + protected final Iterator namesFromString(String name) { + return namesFromString(name, "value"); + } + + protected final Iterator namesFromString(String name, Object value) { + PropertySource propertySource = new MapPropertySource("test", + Collections.singletonMap(name, value)); + return getMapper().map(propertySource, name).stream() + .map((mapping) -> mapping.getConfigurationPropertyName().toString()) + .iterator(); + } + + protected final Iterator namesFromConfiguration(String name) { + return namesFromConfiguration(name, "value"); + } + + protected final Iterator namesFromConfiguration(String name, String value) { + PropertySource propertySource = new MapPropertySource("test", + Collections.singletonMap(name, value)); + return getMapper().map(propertySource, ConfigurationPropertyName.of(name)) + .stream().map((mapping) -> mapping.getPropertySourceName()).iterator(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySourceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySourceTests.java new file mode 100644 index 0000000000..8fc13652dd --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySourceTests.java @@ -0,0 +1,72 @@ +/* + * 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.source; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AliasedConfigurationPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class AliasedConfigurationPropertySourceTests { + + @Test + public void streamShouldInclueAliases() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "bing"); + source.put("foo.baz", "biff"); + ConfigurationPropertySource aliased = source + .withAliases(new ConfigurationPropertyNameAliases("foo.bar", "foo.bar1")); + assertThat(aliased.stream()).containsExactly( + ConfigurationPropertyName.of("foo.bar"), + ConfigurationPropertyName.of("foo.bar1"), + ConfigurationPropertyName.of("foo.baz")); + } + + @Test + public void getConfigurationPropertyShouldConsiderAliases() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "bing"); + source.put("foo.baz", "biff"); + ConfigurationPropertySource aliased = source + .withAliases(new ConfigurationPropertyNameAliases("foo.bar", "foo.bar1")); + assertThat(getValue(aliased, "foo.bar")).isEqualTo("bing"); + assertThat(getValue(aliased, "foo.bar1")).isEqualTo("bing"); + } + + @Test + public void getConfigurationPropertyWhenNotAliasesShouldReturnValue() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "bing"); + source.put("foo.baz", "biff"); + ConfigurationPropertySource aliased = source + .withAliases(new ConfigurationPropertyNameAliases("foo.bar", "foo.bar1")); + assertThat(getValue(aliased, "foo.baz")).isEqualTo("biff"); + } + + private Object getValue(ConfigurationPropertySource source, String name) { + ConfigurationProperty property = source + .getConfigurationProperty(ConfigurationPropertyName.of(name)); + return (property == null ? null : property.getValue()); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliasesTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliasesTests.java new file mode 100644 index 0000000000..c68b303a90 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliasesTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.source; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertyNameAliases}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertyNameAliasesTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWithStringWhenNullNameShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be null"); + new ConfigurationPropertyNameAliases((String) null); + } + + @Test + public void createWithStringShouldAddMapping() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases( + "foo", "bar", "baz"); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + } + + @Test + public void createWithNameShouldAddMapping() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases( + ConfigurationPropertyName.of("foo"), ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + } + + @Test + public void addAliasesFromStringShouldAddMapping() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("foo", "bar", "baz"); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + } + + @Test + public void addAlaisesFromNameShouldAddMapping() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises(ConfigurationPropertyName.of("foo"), + ConfigurationPropertyName.of("bar"), ConfigurationPropertyName.of("baz")); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + } + + @Test + public void addWhenHasExistingShouldAddAdditionalMappings() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("foo", "bar"); + aliases.addAlaises("foo", "baz"); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + } + + @Test + public void getAliasesWhenNotMappedShouldReturnEmptyList() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))).isEmpty(); + } + + @Test + public void getAliasesWhenMappedShouldReturnMapping() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("foo", "bar"); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar")); + } + + @Test + public void getNameForAliasWhenHasMappingShouldReturnName() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("foo", "bar"); + aliases.addAlaises("foo", "baz"); + assertThat((Object) aliases.getNameForAlias(ConfigurationPropertyName.of("bar"))) + .isEqualTo(ConfigurationPropertyName.of("foo")); + assertThat((Object) aliases.getNameForAlias(ConfigurationPropertyName.of("baz"))) + .isEqualTo(ConfigurationPropertyName.of("foo")); + } + + @Test + public void getNameForAliasWhenNotMappedShouldReturnNull() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("foo", "bar"); + assertThat((Object) aliases.getNameForAlias(ConfigurationPropertyName.of("baz"))) + .isNull(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilderTests.java new file mode 100644 index 0000000000..9ccba0c0d9 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilderTests.java @@ -0,0 +1,138 @@ +/* + * 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.source; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Element; +import org.springframework.boot.context.properties.source.ConfigurationPropertyNameBuilder.ElementValueProcessor; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertyNameBuilder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertyNameBuilderTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private ConfigurationPropertyNameBuilder builder; + + @Test + public void createWhenPatternIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Pattern must not be null"); + this.builder = new ConfigurationPropertyNameBuilder((Pattern) null); + } + + @Test + public void createWhenElementProcessorIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Processor must not be null"); + this.builder = new ConfigurationPropertyNameBuilder((ElementValueProcessor) null); + } + + @Test + public void buildShouldCreateName() throws Exception { + this.builder = new ConfigurationPropertyNameBuilder(); + ConfigurationPropertyName expected = ConfigurationPropertyName.of("foo.bar.baz"); + ConfigurationPropertyName name = this.builder.from("foo.bar.baz", '.').build(); + assertThat(name.toString()).isEqualTo(expected.toString()); + } + + @Test + public void buildShouldValidateUsingPattern() { + Pattern pattern = Pattern.compile("[a-z]([a-z0-9\\-])*"); + this.builder = new ConfigurationPropertyNameBuilder(pattern); + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Element value 'foo@!' is not valid"); + this.builder.from("foo@!.bar", '.').build(); + } + + @Test + public void buildWhenHasNoElementsShouldThrowException() throws Exception { + this.builder = new ConfigurationPropertyNameBuilder(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("At least one element must be defined"); + this.builder.build(); + } + + @Test + public void buildShouldUseElementProcessor() throws Exception { + this.builder = new ConfigurationPropertyNameBuilder( + value -> value.replace("-", "")); + ConfigurationPropertyName name = this.builder.from("FOO_THE-BAR", '_').build(); + assertThat(name.toString()).isEqualTo("foo.thebar"); + } + + @Test + public void fromNameShouldSetElements() throws Exception { + this.builder = new ConfigurationPropertyNameBuilder(); + ConfigurationPropertyName name = this.builder.from("foo.bar", '.').build(); + assertThat(name.toString()).isEqualTo("foo.bar"); + } + + @Test + public void fromNameShouldSetIndexedElements() throws Exception { + this.builder = new ConfigurationPropertyNameBuilder(); + assertThat(getElements("foo")).isEqualTo(elements("foo")); + assertThat(getElements("[foo]")).isEqualTo(elements("[foo]")); + assertThat(getElements("foo.bar")).isEqualTo(elements("foo", "bar")); + assertThat(getElements("foo[foo.bar]")).isEqualTo(elements("foo", "[foo.bar]")); + assertThat(getElements("foo.[bar].baz")) + .isEqualTo(elements("foo", "[bar]", "baz")); + } + + @Test + public void fromNameWhenHasExistingShouldSetNewElements() throws Exception { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Existing elements must not be present"); + new ConfigurationPropertyNameBuilder().from("foo.bar", '.').from("baz", '.') + .build(); + } + + @Test + public void appendShouldAppendElement() throws Exception { + this.builder = new ConfigurationPropertyNameBuilder(); + ConfigurationPropertyName name = this.builder.from("foo.bar", '.').append("baz") + .build(); + assertThat(name.toString()).isEqualTo("foo.bar.baz"); + } + + private List elements(String... elements) { + return Arrays.stream(elements).map(Element::new).collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") + private List getElements(String name) { + ConfigurationPropertyNameBuilder builder = this.builder.from(name, '.'); + return (List) ReflectionTestUtils.getField(builder, "elements"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java new file mode 100644 index 0000000000..a0df87b141 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java @@ -0,0 +1,326 @@ +/* + * 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.source; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Element; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link ConfigurationPropertyName}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertyNameTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void ofNameShouldNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be null"); + ConfigurationPropertyName.of((String) null); + } + + @Test + public void ofNameShouldNotStartWithNumber() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("1foo"); + } + + @Test + public void ofNameShouldNotStartWithDash() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("-foo"); + } + + @Test + public void ofNameShouldNotStartWithDot() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not start with '.'"); + ConfigurationPropertyName.of(".foo"); + } + + @Test + public void ofNameShouldNotEndWithDot() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not end with '.'"); + ConfigurationPropertyName.of("foo."); + } + + @Test + public void ofNameShouldNotContainUppercase() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("fOo"); + } + + @Test + public void ofNameShouldNotContainInvalidChars() throws Exception { + String invalid = "_@$%*+=':;"; + for (char c : invalid.toCharArray()) { + try { + ConfigurationPropertyName.of("foo" + c); + fail("Did not throw for invalid char " + c); + } + catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).contains("is not valid"); + } + } + } + + @Test + public void ofNameWhenSimple() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("name"); + assertThat(name.toString()).isEqualTo("name"); + assertThat((Object) name.getParent()).isNull(); + assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("name"); + } + + @Test + public void ofNameWhenRunOnAssociative() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[bar]"); + assertThat(name.toString()).isEqualTo("foo[bar]"); + assertThat(name.getParent().toString()).isEqualTo("foo"); + assertThat(name.getElement().toString()).isEqualTo("[bar]"); + } + + @Test + public void ofNameWhenDotOnAssociative() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar"); + assertThat(name.toString()).isEqualTo("foo.bar"); + assertThat(name.getParent().toString()).isEqualTo("foo"); + assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("bar"); + } + + @Test + public void ofNameWhenDotAndAssociative() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.[bar]"); + assertThat(name.toString()).isEqualTo("foo[bar]"); + assertThat(name.getParent().toString()).isEqualTo("foo"); + assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("bar"); + } + + @Test + public void ofNameWhenDoubleRunOnAndAssociative() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[bar]baz"); + assertThat(name.toString()).isEqualTo("foo[bar].baz"); + assertThat(name.getParent().toString()).isEqualTo("foo[bar]"); + assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("baz"); + } + + @Test + public void ofNameWhenDoubleDotAndAssociative() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.[bar].baz"); + assertThat(name.toString()).isEqualTo("foo[bar].baz"); + assertThat(name.getParent().toString()).isEqualTo("foo[bar]"); + assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("baz"); + } + + @Test + public void ofNameWhenMissingCloseBracket() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("[bar"); + } + + @Test + public void ofNameWhenMissingOpenBracket() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("bar]"); + } + + @Test + public void ofNameWithWhitespaceInName() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("foo. bar"); + } + + @Test + public void ofNameWithWhitespaceInAssociativeElement() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[b a r]"); + assertThat(name.toString()).isEqualTo("foo[b a r]"); + assertThat(name.getParent().toString()).isEqualTo("foo"); + assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("b a r"); + } + + @Test + public void ofNameWithUppercaseInAssociativeElement() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[BAR]"); + assertThat(name.toString()).isEqualTo("foo[BAR]"); + assertThat(name.getParent().toString()).isEqualTo("foo"); + assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("BAR"); + } + + @Test + public void equalsAndHashCode() throws Exception { + ConfigurationPropertyName name1 = ConfigurationPropertyName.of("foo[bar]"); + ConfigurationPropertyName name2 = ConfigurationPropertyName.of("foo[bar]"); + ConfigurationPropertyName name3 = ConfigurationPropertyName.of("foo.bar"); + ConfigurationPropertyName name4 = ConfigurationPropertyName.of("f-o-o.b-a-r"); + ConfigurationPropertyName name5 = ConfigurationPropertyName.of("foo[BAR]"); + ConfigurationPropertyName name6 = ConfigurationPropertyName.of("oof[bar]"); + ConfigurationPropertyName name7 = ConfigurationPropertyName.of("foo.bar"); + ConfigurationPropertyName name8 = new ConfigurationPropertyName( + new ConfigurationPropertyName(null, new Element("FOO")), + new Element("BAR")); + assertThat(name1.hashCode()).isEqualTo(name2.hashCode()); + assertThat(name1.hashCode()).isEqualTo(name2.hashCode()); + assertThat(name1.hashCode()).isEqualTo(name3.hashCode()); + assertThat(name1.hashCode()).isEqualTo(name4.hashCode()); + assertThat(name7.hashCode()).isEqualTo(name8.hashCode()); + assertThat((Object) name1).isEqualTo(name1); + assertThat((Object) name1).isEqualTo(name2); + assertThat((Object) name1).isEqualTo(name3); + assertThat((Object) name1).isEqualTo(name4); + assertThat((Object) name1).isNotEqualTo(name5); + assertThat((Object) name1).isNotEqualTo(name6); + assertThat((Object) name7).isEqualTo(name8); + } + + @Test + public void elementNameShouldNotIncludeAngleBrackets() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("[foo]"); + assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("foo"); + } + + @Test + public void elementNameShouldNotIncludeDashes() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("f-o-o"); + assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("foo"); + } + + @Test + public void streamShouldReturnElements() throws Exception { + assertThat(streamElements("foo.bar")).containsExactly("foo", "bar"); + assertThat(streamElements("foo[0]")).containsExactly("foo", "[0]"); + assertThat(streamElements("foo.[0]")).containsExactly("foo", "[0]"); + assertThat(streamElements("foo[baz]")).containsExactly("foo", "[baz]"); + assertThat(streamElements("foo.baz")).containsExactly("foo", "baz"); + assertThat(streamElements("foo[baz].bar")).containsExactly("foo", "[baz]", "bar"); + assertThat(streamElements("foo.baz.bar")).containsExactly("foo", "baz", "bar"); + assertThat(streamElements("foo.baz-bar")).containsExactly("foo", "baz-bar"); + } + + private Iterator streamElements(String name) { + return ConfigurationPropertyName.of(name).stream().map((e) -> e.toString()) + .iterator(); + } + + @Test + public void elementIsIndexedWhenIndexedShouldReturnTrue() throws Exception { + assertThat(ConfigurationPropertyName.of("foo[0]").getElement().isIndexed()) + .isTrue(); + } + + @Test + public void elementIsIndexedWhenNotIndexedShouldReturnFalse() throws Exception { + assertThat(ConfigurationPropertyName.of("foo.bar").getElement().isIndexed()) + .isFalse(); + } + + @Test + public void isAncestorOfWhenSameShouldReturnFalse() throws Exception { + ConfigurationPropertyName parent = ConfigurationPropertyName.of("foo"); + assertThat(parent.isAncestorOf(parent)).isFalse(); + } + + @Test + public void isAncestorOfWhenParentShouldReturnFalse() throws Exception { + ConfigurationPropertyName parent = ConfigurationPropertyName.of("foo"); + ConfigurationPropertyName child = ConfigurationPropertyName.of("foo.bar"); + assertThat(parent.isAncestorOf(child)).isTrue(); + assertThat(child.isAncestorOf(parent)).isFalse(); + } + + @Test + public void isAncestorOfWhenGrandparentShouldReturnFalse() throws Exception { + ConfigurationPropertyName parent = ConfigurationPropertyName.of("foo"); + ConfigurationPropertyName grandchild = ConfigurationPropertyName + .of("foo.bar.baz"); + assertThat(parent.isAncestorOf(grandchild)).isTrue(); + assertThat(grandchild.isAncestorOf(parent)).isFalse(); + } + + @Test + public void appendWhenNotIndexedShouldAppendWithDot() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + assertThat(name.append("bar").toString()).isEqualTo("foo.bar"); + } + + @Test + public void appendWhenIndexedShouldAppendWithBrackets() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo") + .append("[bar]"); + assertThat(name.getElement().isIndexed()).isTrue(); + assertThat(name.toString()).isEqualTo("foo[bar]"); + } + + @Test + public void appendWhenElementNameIsNotValidShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Element value '1bar' is not valid"); + ConfigurationPropertyName.of("foo").append("1bar"); + } + + @Test + public void appendWhenElementNameIsNullShouldReturnName() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + assertThat((Object) name.append((String) null)).isSameAs(name); + } + + @Test + public void compareShouldSortNames() throws Exception { + List names = new ArrayList<>(); + names.add(ConfigurationPropertyName.of("foo[10]")); + names.add(ConfigurationPropertyName.of("foo.bard")); + names.add(ConfigurationPropertyName.of("foo[2]")); + names.add(ConfigurationPropertyName.of("foo.bar")); + names.add(ConfigurationPropertyName.of("foo.baz")); + names.add(ConfigurationPropertyName.of("foo")); + Collections.sort(names); + assertThat(names.stream().map(ConfigurationPropertyName::toString) + .collect(Collectors.toList())).containsExactly("foo", "foo[2]", "foo[10]", + "foo.bar", "foo.bard", "foo.baz"); + } + + @Test + public void ofNameCanBeEmpty() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of(""); + assertThat(name.toString()).isEqualTo(""); + assertThat(name.append("foo").toString()).isEqualTo("foo"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySourceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySourceTests.java new file mode 100644 index 0000000000..b43a91fb25 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySourceTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.source; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertySourcesPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertySourcesPropertySourceTests { + + private List configurationSources = new ArrayList<>(); + + private ConfigurationPropertySourcesPropertySource propertySource = new ConfigurationPropertySourcesPropertySource( + "test", this.configurationSources); + + @Test + public void getPropertyShouldReturnValue() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz")); + assertThat(this.propertySource.getProperty("foo.bar")).isEqualTo("baz"); + } + + @Test + public void getPropertyWhenNameIsNotValidShouldReturnNull() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz")); + assertThat(this.propertySource.getProperty("FOO.B-A-R")).isNull(); + assertThat(this.propertySource.getProperty("FOO.B A R")).isNull(); + assertThat(this.propertySource.getProperty(".foo.bar")).isNull(); + } + + @Test + public void getPropertyWhenMultipleShouldReturnFirst() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz")); + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "bill")); + assertThat(this.propertySource.getProperty("foo.bar")).isEqualTo("baz"); + } + + @Test + public void getPropertyWhenNoneShouldReturnFirst() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz")); + assertThat(this.propertySource.getProperty("foo.foo")).isNull(); + } + + @Test + public void getPropertyOriginShouldReturnOrigin() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz", "line1")); + assertThat(this.propertySource.getOrigin("foo.bar").toString()) + .isEqualTo("line1"); + } + + @Test + public void getPropertyOriginWhenMissingShouldReturnNull() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz", "line1")); + assertThat(this.propertySource.getOrigin("foo.foo")).isNull(); + } + + @Test + public void getNameShouldReturnName() throws Exception { + assertThat(this.propertySource.getName()).isEqualTo("test"); + } + + @Test + public void getSourceShouldReturnSource() throws Exception { + assertThat(this.propertySource.getSource()).isSameAs(this.configurationSources); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java new file mode 100644 index 0000000000..554600c23b --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java @@ -0,0 +1,180 @@ +/* + * 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.source; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +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.PropertyResolver; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySourcesPropertyResolver; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertySources}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertySourcesTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenPropertySourcesIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("PropertySources must not be null"); + new ConfigurationPropertySources(null); + } + + @Test + public void iteratorShouldAdaptPropertySource() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new MapPropertySource("test", + Collections.singletonMap("a", "b"))); + Iterator iterator = new ConfigurationPropertySources( + sources).iterator(); + assertThat(iterator.next() + .getConfigurationProperty(ConfigurationPropertyName.of("a")).getValue()) + .isEqualTo("b"); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + public void iteratorShouldAdaptSystemEnvironmentPropertySource() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addLast(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("SERVER_PORT", "1234"))); + Iterator iterator = new ConfigurationPropertySources( + sources).iterator(); + assertThat( + iterator.next() + .getConfigurationProperty( + ConfigurationPropertyName.of("server.port")) + .getValue()).isEqualTo("1234"); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + public void iteratorShouldAdaptMultiplePropertySources() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addLast(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("SERVER_PORT", "1234"))); + sources.addLast(new MapPropertySource("test1", + Collections.singletonMap("server.po-rt", "4567"))); + sources.addLast(new MapPropertySource("test2", + Collections.singletonMap("a", "b"))); + Iterator iterator = new ConfigurationPropertySources( + sources).iterator(); + assertThat( + iterator.next() + .getConfigurationProperty( + ConfigurationPropertyName.of("server.port")) + .getValue()).isEqualTo("1234"); + assertThat( + iterator.next() + .getConfigurationProperty( + ConfigurationPropertyName.of("server.port")) + .getValue()).isEqualTo("4567"); + assertThat(iterator.next() + .getConfigurationProperty(ConfigurationPropertyName.of("a")).getValue()) + .isEqualTo("b"); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + public void attachShouldAddAdapterAtBegining() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addLast(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("SERVER_PORT", "1234"))); + sources.addLast(new MapPropertySource("config", + Collections.singletonMap("server.port", "4568"))); + assertThat(sources.size()).isEqualTo(2); + ConfigurationPropertySources.attach(sources); + PropertyResolver resolver = new PropertySourcesPropertyResolver(sources); + assertThat(resolver.getProperty("server.port")).isEqualTo("1234"); + assertThat(sources.size()).isEqualTo(3); + } + + @Test + public void getWhenAttachedShouldReturnAttached() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new MapPropertySource("test", + Collections.singletonMap("a", "b"))); + ConfigurationPropertySources attached = ConfigurationPropertySources + .attach(sources); + assertThat(ConfigurationPropertySources.get(sources)).isSameAs(attached); + } + + @Test + public void getWhenNotAttachedShouldReturnNew() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new MapPropertySource("test", + Collections.singletonMap("a", "b"))); + assertThat(ConfigurationPropertySources.get(sources)).isNotNull(); + assertThat(sources.size()).isEqualTo(1); + } + + @Test + public void environmentProperyExpansionShouldWorkWhenAttached() throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + Map source = new LinkedHashMap<>(); + source.put("fooBar", "Spring ${barBaz} ${bar-baz}"); + source.put("barBaz", "Boot"); + PropertySource propertySource = new MapPropertySource("test", source); + environment.getPropertySources().addFirst(propertySource); + ConfigurationPropertySources.attach(environment); + assertThat(environment.getProperty("foo-bar")).isEqualTo("Spring Boot Boot"); + } + + @Test + public void environmentSourceShouldBeFlattened() throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("foo", + Collections.singletonMap("foo", "bar"))); + environment.getPropertySources().addFirst(new MapPropertySource("far", + Collections.singletonMap("far", "far"))); + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new PropertySource("env", environment) { + + @Override + public String getProperty(String key) { + return this.source.getProperty(key); + } + + }); + sources.addLast(new MapPropertySource("baz", + Collections.singletonMap("baz", "barf"))); + ConfigurationPropertySources configurationSources = ConfigurationPropertySources + .get(sources); + assertThat(configurationSources.iterator()).hasSize(5); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyTests.java new file mode 100644 index 0000000000..5e84e3e1e3 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyTests.java @@ -0,0 +1,97 @@ +/* + * 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.source; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ConfigurationProperty}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertyTests { + + 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 ConfigurationProperty(null, "bar", null); + } + + @Test + public void createWhenValueIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Value must not be null"); + new ConfigurationProperty(NAME, null, null); + } + + @Test + public void getNameShouldReturnName() throws Exception { + ConfigurationProperty property = ConfigurationProperty.of(NAME, "foo", null); + assertThat((Object) property.getName()).isEqualTo(NAME); + } + + @Test + public void getValueShouldReturnValue() throws Exception { + ConfigurationProperty property = ConfigurationProperty.of(NAME, "foo", null); + assertThat(property.getValue()).isEqualTo("foo"); + } + + @Test + public void getPropertyOrginShouldReturnValuePropertyOrigin() throws Exception { + Origin origin = mock(Origin.class); + OriginProvider property = ConfigurationProperty.of(NAME, "foo", origin); + assertThat(property.getOrigin()).isEqualTo(origin); + } + + @Test + public void equalsAndHashCode() throws Exception { + ConfigurationProperty property1 = new ConfigurationProperty( + ConfigurationPropertyName.of("foo"), "bar", null); + ConfigurationProperty property2 = new ConfigurationProperty( + ConfigurationPropertyName.of("foo"), "bar", null); + ConfigurationProperty property3 = new ConfigurationProperty( + ConfigurationPropertyName.of("foo"), "baz", null); + ConfigurationProperty property4 = new ConfigurationProperty( + ConfigurationPropertyName.of("baz"), "bar", null); + assertThat(property1.hashCode()).isEqualTo(property2.hashCode()); + assertThat(property1).isEqualTo(property2).isNotEqualTo(property3) + .isNotEqualTo(property4); + } + + @Test + public void toStringShouldReturnValue() throws Exception { + ConfigurationProperty property = ConfigurationProperty.of(NAME, "foo", null); + assertThat(property.toString()).contains("name").contains("value"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java new file mode 100644 index 0000000000..c03b0968b3 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java @@ -0,0 +1,70 @@ +/* + * 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.source; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultPropertyMapper}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class DefaultPropertyMapperTests extends AbstractPropertyMapperTests { + + private DefaultPropertyMapper mapper = new DefaultPropertyMapper(); + + @Override + protected PropertyMapper getMapper() { + return this.mapper; + } + + @Test + public void mapFromStringShouldReturnBestGuess() throws Exception { + assertThat(namesFromString("server")).containsExactly("server"); + assertThat(namesFromString("server.port")).containsExactly("server.port"); + assertThat(namesFromString("host[0]")).containsExactly("host[0]"); + assertThat(namesFromString("host[0][1]")).containsExactly("host[0][1]"); + assertThat(namesFromString("host[0].name")).containsExactly("host[0].name"); + assertThat(namesFromString("host.f00.name")).containsExactly("host.f00.name"); + assertThat(namesFromString("my.host-name")).containsExactly("my.host-name"); + assertThat(namesFromString("my.hostName")).containsExactly("my.hostname"); + assertThat(namesFromString("my.HOST_NAME")).containsExactly("my.hostname"); + assertThat(namesFromString("s[!@#$%^&*()=+]e-rVeR")) + .containsExactly("s[!@#$%^&*()=+].e-rver"); + assertThat(namesFromString("host[FOO].name")).containsExactly("host[FOO].name"); + } + + @Test + public void mapFromConfigurationShouldReturnBestGuess() throws Exception { + assertThat(namesFromConfiguration("server")).containsExactly("server"); + assertThat(namesFromConfiguration("server.port")).containsExactly("server.port"); + assertThat(namesFromConfiguration("host[0]")).containsExactly("host[0]"); + assertThat(namesFromConfiguration("host[0][1]")).containsExactly("host[0][1]"); + assertThat(namesFromConfiguration("host[0].name")) + .containsExactly("host[0].name"); + assertThat(namesFromConfiguration("host.f00.name")) + .containsExactly("host.f00.name"); + assertThat(namesFromConfiguration("my.host-name")) + .containsExactly("my.host-name"); + assertThat(namesFromConfiguration("host[FOO].name")) + .containsExactly("host[FOO].name"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSourceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSourceTests.java new file mode 100644 index 0000000000..b0b760dc49 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSourceTests.java @@ -0,0 +1,89 @@ +/* + * 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.source; + +import java.util.Objects; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link FilteredConfigurationPropertiesSource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class FilteredConfigurationPropertiesSourceTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenSourceIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Source must not be null"); + new FilteredConfigurationPropertiesSource(null, Objects::nonNull); + } + + @Test + public void createWhenFilterIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Filter must not be null"); + new FilteredConfigurationPropertiesSource(new MockConfigurationPropertySource(), + null); + } + + @Test + public void iteratorShouldFilterNames() throws Exception { + MockConfigurationPropertySource source = createTestSource(); + ConfigurationPropertySource filtered = source.filter(this::noBrackets); + assertThat(filtered.iterator()).extracting(ConfigurationPropertyName::toString) + .containsExactly("a", "b", "c"); + } + + @Test + public void getValueShouldFilterNames() throws Exception { + MockConfigurationPropertySource source = createTestSource(); + ConfigurationPropertySource filtered = source.filter(this::noBrackets); + ConfigurationPropertyName name = ConfigurationPropertyName.of("a"); + assertThat(source.getConfigurationProperty(name).getValue()).isEqualTo("1"); + assertThat(filtered.getConfigurationProperty(name).getValue()).isEqualTo("1"); + ConfigurationPropertyName bracketName = ConfigurationPropertyName.of("a[1]"); + assertThat(source.getConfigurationProperty(bracketName).getValue()) + .isEqualTo("2"); + assertThat(filtered.getConfigurationProperty(bracketName)).isNull(); + + } + + private MockConfigurationPropertySource createTestSource() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("a", "1"); + source.put("a[1]", "2"); + source.put("b", "3"); + source.put("b[1]", "4"); + source.put("c", "5"); + return source; + } + + private boolean noBrackets(ConfigurationPropertyName name) { + return name.toString().indexOf("[") == -1; + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySourceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySourceTests.java new file mode 100644 index 0000000000..b5552dac41 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySourceTests.java @@ -0,0 +1,118 @@ +/* + * 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.source; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MapConfigurationPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class MapConfigurationPropertySourceTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenMapIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Map must not be null"); + new MapConfigurationPropertySource(null); + } + + @Test + public void createWhenMapHasEntriesShouldAdaptMap() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("foo.BAR", "spring"); + map.put(ConfigurationPropertyName.of("foo.baz"), "boot"); + MapConfigurationPropertySource source = new MapConfigurationPropertySource(map); + assertThat(getValue(source, "foo.bar")).isEqualTo("spring"); + assertThat(getValue(source, "foo.baz")).isEqualTo("boot"); + } + + @Test + public void putAllWhenMapIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Map must not be null"); + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.putAll(null); + } + + @Test + public void putAllShouldPutEntries() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("foo.BAR", "spring"); + map.put("foo.baz", "boot"); + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.putAll(map); + assertThat(getValue(source, "foo.bar")).isEqualTo("spring"); + assertThat(getValue(source, "foo.baz")).isEqualTo("boot"); + } + + @Test + public void putShouldPutEntry() throws Exception { + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.put("foo.bar", "baz"); + assertThat(getValue(source, "foo.bar")).isEqualTo("baz"); + } + + @Test + public void getConfigurationPropertyShouldGetFromMemory() throws Exception { + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.put("foo.bar", "baz"); + assertThat(getValue(source, "foo.bar")).isEqualTo("baz"); + source.put("foo.bar", "big"); + assertThat(getValue(source, "foo.bar")).isEqualTo("big"); + } + + @Test + public void iteratorShouldGetFromMemory() throws Exception { + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.put("foo.BAR", "spring"); + source.put("foo.baz", "boot"); + assertThat(source.iterator()).containsExactly( + ConfigurationPropertyName.of("foo.bar"), + ConfigurationPropertyName.of("foo.baz")); + } + + @Test + public void streamShouldGetFromMemory() throws Exception { + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.put("foo.BAR", "spring"); + source.put("foo.baz", "boot"); + assertThat(source.stream()).containsExactly( + ConfigurationPropertyName.of("foo.bar"), + ConfigurationPropertyName.of("foo.baz")); + + } + + private Object getValue(ConfigurationPropertySource source, String name) { + ConfigurationProperty property = source + .getConfigurationProperty(ConfigurationPropertyName.of(name)); + return (property == null ? null : property.getValue()); + }; + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java new file mode 100644 index 0000000000..94e01c9e72 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java @@ -0,0 +1,100 @@ +/* + * 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.source; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.boot.origin.MockOrigin; +import org.springframework.boot.origin.OriginTrackedValue; + +/** + * Mock {@link ConfigurationPropertySource} implementation used for testing. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class MockConfigurationPropertySource implements ConfigurationPropertySource { + + private final Map map = new LinkedHashMap<>(); + + private boolean nonIterable; + + public MockConfigurationPropertySource() { + } + + public MockConfigurationPropertySource(String configurationPropertyName, + Object value) { + this(configurationPropertyName, value, null); + } + + public MockConfigurationPropertySource(String configurationPropertyName, Object value, + String origin) { + put(ConfigurationPropertyName.of(configurationPropertyName), + OriginTrackedValue.of(value, MockOrigin.of(origin))); + } + + public void put(String name, String value) { + put(ConfigurationPropertyName.of(name), value); + } + + public void put(ConfigurationPropertyName name, String value) { + put(name, OriginTrackedValue.of(value)); + } + + private void put(ConfigurationPropertyName name, OriginTrackedValue value) { + this.map.put(name, value); + } + + public void setNonIterable(boolean nonIterable) { + this.nonIterable = nonIterable; + } + + @Override + public Iterator iterator() { + if (this.nonIterable) { + return Collections.emptyList().iterator(); + } + return this.map.keySet().iterator(); + } + + @Override + public Stream stream() { + if (this.nonIterable) { + return Collections.emptyList().stream(); + } + return this.map.keySet().stream(); + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + OriginTrackedValue result = this.map.get(name); + if (result == null) { + result = findValue(name); + } + return ConfigurationProperty.of(name, result); + } + + private OriginTrackedValue findValue(ConfigurationPropertyName name) { + return this.map.get(name); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySourceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySourceTests.java new file mode 100644 index 0000000000..13a43baa37 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySourceTests.java @@ -0,0 +1,265 @@ +/* + * 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.source; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PropertySourceConfigurationPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class PropertySourceConfigurationPropertySourceTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenPropertySourceIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("PropertySource must not be null"); + new PropertySourceConfigurationPropertySource(null, mock(PropertyMapper.class)); + } + + @Test + public void createWhenMapperIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Mapper must not be null"); + new PropertySourceConfigurationPropertySource(mock(PropertySource.class), null); + } + + @Test + public void iteratorWhenNonEnumerbleShouldReturnEmptyIterator() throws Exception { + Map source = new LinkedHashMap<>(); + PropertySource propertySource = new NonEnumerablePropertySource<>( + new MapPropertySource("test", source)); + TestPropertyMapper mapper = new TestPropertyMapper(); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.iterator()).isEmpty(); + } + + @Test + public void iteratorShouldAdaptNames() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key1", "value1"); + source.put("key2", "value2"); + source.put("key3", "value3"); + source.put("key4", "value4"); + PropertySource propertySource = new MapPropertySource("test", source); + TestPropertyMapper mapper = new TestPropertyMapper(); + mapper.addFromProperySource("key1", "my.key1"); + mapper.addFromProperySource("key2", "my.key2a", "my.key2b"); + mapper.addFromProperySource("key4", "my.key4"); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.iterator()).extracting(Object::toString) + .containsExactly("my.key1", "my.key2a", "my.key2b", "my.key4"); + } + + @Test + public void getValueShouldUseDirectMapping() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key1", "value1"); + source.put("key2", "value2"); + source.put("key3", "value3"); + PropertySource propertySource = new NonEnumerablePropertySource<>( + new MapPropertySource("test", source)); + TestPropertyMapper mapper = new TestPropertyMapper(); + ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); + mapper.addFromConfigurationProperty(name, "key2"); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("value2"); + } + + @Test + public void getValueShouldFallbackToEnumerableMapping() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key1", "value1"); + source.put("key2", "value2"); + source.put("key3", "value3"); + PropertySource propertySource = new MapPropertySource("test", source); + TestPropertyMapper mapper = new TestPropertyMapper(); + mapper.addFromProperySource("key1", "my.missing"); + mapper.addFromProperySource("key2", "my.k-e-y"); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); + assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("value2"); + } + + @Test + public void getValueShouldUseExtractor() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key", "value"); + PropertySource propertySource = new NonEnumerablePropertySource<>( + new MapPropertySource("test", source)); + TestPropertyMapper mapper = new TestPropertyMapper(); + ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); + mapper.addFromConfigurationProperty(name, "key", + (value) -> value.toString().replace("ue", "let")); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("vallet"); + } + + @Test + public void getValueOrigin() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key", "value"); + PropertySource propertySource = new MapPropertySource("test", source); + TestPropertyMapper mapper = new TestPropertyMapper(); + ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); + mapper.addFromConfigurationProperty(name, "key"); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.getConfigurationProperty(name).getOrigin().toString()) + .isEqualTo("\"key\" from property source \"test\""); + } + + @Test + public void getValueWhenOriginCapableShouldIncludeSourceOrigin() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key", "value"); + PropertySource propertySource = new OriginCapablePropertySource<>( + new MapPropertySource("test", source)); + TestPropertyMapper mapper = new TestPropertyMapper(); + ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); + mapper.addFromConfigurationProperty(name, "key"); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.getConfigurationProperty(name).getOrigin().toString()) + .isEqualTo("TestOrigin key"); + } + + /** + * Test {@link PropertySource} that doesn't extend {@link EnumerablePropertySource}. + */ + private static class NonEnumerablePropertySource extends PropertySource { + + private final PropertySource propertySource; + + NonEnumerablePropertySource(PropertySource propertySource) { + super(propertySource.getName(), propertySource.getSource()); + this.propertySource = propertySource; + } + + @Override + public Object getProperty(String name) { + return this.propertySource.getProperty(name); + } + + } + + /** + * Test {@link PropertySource} that's also a {@link OriginLookup}. + */ + private static class OriginCapablePropertySource extends PropertySource + implements OriginLookup { + + private final PropertySource propertySource; + + OriginCapablePropertySource(PropertySource propertySource) { + super(propertySource.getName(), propertySource.getSource()); + this.propertySource = propertySource; + } + + @Override + public Object getProperty(String name) { + return this.propertySource.getProperty(name); + } + + @Override + public Origin getOrigin(String name) { + return new Origin() { + + @Override + public String toString() { + return "TestOrigin " + name; + } + + }; + } + + } + + /** + * Test {@link PropertyMapper} implementation. + */ + private static class TestPropertyMapper implements PropertyMapper { + + private MultiValueMap fromSource = new LinkedMultiValueMap<>(); + + private MultiValueMap fromConfig = new LinkedMultiValueMap<>(); + + public void addFromProperySource(String from, String... to) { + for (String configurationPropertyName : to) { + this.fromSource.add(from, new PropertyMapping(from, + ConfigurationPropertyName.of(configurationPropertyName))); + } + } + + public void addFromConfigurationProperty(ConfigurationPropertyName from, + String... to) { + for (String propertySourceName : to) { + this.fromConfig.add(from, new PropertyMapping(propertySourceName, from)); + } + } + + public void addFromConfigurationProperty(ConfigurationPropertyName from, + String to, Function extractor) { + this.fromConfig.add(from, new PropertyMapping(to, from, extractor)); + } + + @Override + public List map(PropertySource propertySource, + String propertySourceName) { + return this.fromSource.getOrDefault(propertySourceName, + Collections.emptyList()); + } + + @Override + public List map(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName) { + return this.fromConfig.getOrDefault(configurationPropertyName, + Collections.emptyList()); + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java new file mode 100644 index 0000000000..7033b06548 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java @@ -0,0 +1,131 @@ +/* + * 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.source; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SystemEnvironmentPropertyMapper}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class SystemEnvironmentPropertyMapperTests extends AbstractPropertyMapperTests { + + private SystemEnvironmentPropertyMapper mapper = new SystemEnvironmentPropertyMapper(); + + @Override + protected PropertyMapper getMapper() { + return this.mapper; + } + + @Test + public void mapFromStringShouldReturnBestGuess() throws Exception { + assertThat(namesFromString("SERVER")).containsExactly("server"); + assertThat(namesFromString("SERVER_PORT")).containsExactly("server.port"); + assertThat(namesFromString("HOST_0")).containsExactly("host[0]"); + assertThat(namesFromString("HOST_0_1")).containsExactly("host[0][1]"); + assertThat(namesFromString("HOST_0_NAME")).containsExactly("host[0].name"); + assertThat(namesFromString("HOST_F00_NAME")).containsExactly("host.f00.name"); + assertThat(namesFromString("S-ERVER")).containsExactly("s-erver"); + assertThat(namesFromString("SERVERS__", "1,2,3")).containsExactly("servers[0]", + "servers[1]", "servers[2]"); + assertThat(namesFromString("SERVERS_0__", "1,2,3")) + .containsExactly("servers[0][0]", "servers[0][1]", "servers[0][2]"); + } + + @Test + public void mapFromConfigurationShouldReturnBestGuess() throws Exception { + assertThat(namesFromConfiguration("server")).containsExactly("SERVER"); + assertThat(namesFromConfiguration("server.port")).containsExactly("SERVER_PORT"); + assertThat(namesFromConfiguration("host[0]")).containsExactly("HOST_0"); + assertThat(namesFromConfiguration("host[0][1]")).containsExactly("HOST_0_1"); + assertThat(namesFromConfiguration("host[0].name")).containsExactly("HOST_0_NAME"); + assertThat(namesFromConfiguration("host.f00.name")) + .containsExactly("HOST_F00_NAME"); + assertThat(namesFromConfiguration("foo.the-bar")).containsExactly("FOO_THEBAR"); + } + + @Test + public void mapFromStringWhenListShortcutShouldExtractValues() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("SERVER__", "foo,bar,baz"); + PropertySource propertySource = new MapPropertySource("test", source); + List mappings = this.mapper.map(propertySource, "SERVER__"); + List result = new ArrayList<>(); + for (PropertyMapping mapping : mappings) { + Object value = propertySource.getProperty(mapping.getPropertySourceName()); + value = mapping.getValueExtractor().apply(value); + result.add(value); + } + assertThat(result).containsExactly("foo", "bar", "baz"); + } + + @Test + public void mapFromConfigurationShouldIncludeShortcutAndExtractValues() + throws Exception { + Map source = new LinkedHashMap<>(); + source.put("SERVER__", "foo,bar,baz"); + PropertySource propertySource = new MapPropertySource("test", source); + List mappings = this.mapper.map(propertySource, + ConfigurationPropertyName.of("server[1]")); + List result = new ArrayList<>(); + for (PropertyMapping mapping : mappings) { + Object value = propertySource.getProperty(mapping.getPropertySourceName()); + value = mapping.getValueExtractor().apply(value); + if (value != null) { + result.add(value); + } + } + assertThat(result).containsExactly("bar"); + } + + @Test + public void underscoreShouldNotMapToEmptyString() throws Exception { + Map source = new LinkedHashMap<>(); + PropertySource propertySource = new MapPropertySource("test", source); + List mappings = this.mapper.map(propertySource, "_"); + boolean applicable = false; + for (PropertyMapping mapping : mappings) { + applicable = mapping.isApplicable(ConfigurationPropertyName.of("")); + } + assertThat(applicable).isFalse(); + } + + @Test + public void underscoreWithWhitespaceShouldNotMapToEmptyString() throws Exception { + Map source = new LinkedHashMap<>(); + PropertySource propertySource = new MapPropertySource("test", source); + List mappings = this.mapper.map(propertySource, " _"); + boolean applicable = false; + for (PropertyMapping mapping : mappings) { + applicable = mapping.isApplicable(ConfigurationPropertyName.of("")); + } + assertThat(applicable).isFalse(); + } + +} From 813128a410e87f08c472720b3b5325b7d1a09999 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 17 Apr 2017 11:43:09 -0700 Subject: [PATCH 04/19] Add support for configuration properties binding Create a new `Binder` class specifically designed to bind properties from one or more `ConfigurationPropertySources` to an object. The binder provides a replacement for `RelaxedBinder` and attempts to fix the limitations of the previous solution. Closes gh-8868 --- .../properties/bind/AbstractBindHandler.java | 73 ++ .../properties/bind/AggregateBinder.java | 130 +++ .../bind/AggregateElementBinder.java | 53 ++ .../context/properties/bind/ArrayBinder.java | 62 ++ .../context/properties/bind/BeanBinder.java | 43 + .../properties/bind/BeanPropertyBinder.java | 37 + .../properties/bind/BeanPropertyName.java | 59 ++ .../context/properties/bind/BindContext.java | 66 ++ .../properties/bind/BindException.java | 85 ++ .../context/properties/bind/BindHandler.java | 92 ++ .../context/properties/bind/BindResult.java | 167 ++++ .../context/properties/bind/Bindable.java | 235 +++++ .../boot/context/properties/bind/Binder.java | 435 ++++++++++ .../properties/bind/CollectionBinder.java | 59 ++ .../bind/IndexedElementsBinder.java | 137 +++ .../properties/bind/JavaBeanBinder.java | 309 +++++++ .../context/properties/bind/MapBinder.java | 146 ++++ .../properties/bind/PlaceholdersResolver.java | 44 + .../PropertySourcesPlaceholdersResolver.java | 85 ++ ...boundConfigurationPropertiesException.java | 57 ++ .../bind/convert/BinderConversionService.java | 147 ++++ .../convert/InetAddressToStringConverter.java | 37 + .../bind/convert/PropertyEditorConverter.java | 83 ++ .../convert/ResolvableTypeDescriptor.java | 57 ++ .../convert/StringToCharArrayConverter.java | 33 + .../convert/StringToEnumConverterFactory.java | 88 ++ .../convert/StringToInetAddressConverter.java | 43 + .../properties/bind/convert/package-info.java | 20 + .../bind/handler/IgnoreErrorsBindHandler.java | 48 ++ .../IgnoreNestedPropertiesBindHandler.java | 51 ++ .../handler/NoUnboundElementsBindHandler.java | 91 ++ .../properties/bind/handler/package-info.java | 21 + .../context/properties/bind/package-info.java | 20 + .../validation/BindValidationException.java | 47 + .../validation/OriginTrackedFieldError.java | 61 ++ .../validation/ValidationBindHandler.java | 132 +++ .../bind/validation/ValidationErrors.java | 137 +++ .../bind/validation/package-info.java | 20 + .../properties/bind/ArrayBinderTests.java | 274 ++++++ .../bind/BeanPropertyNameTests.java | 41 + .../properties/bind/BindResultTests.java | 234 +++++ .../properties/bind/BindableTests.java | 210 +++++ .../context/properties/bind/BinderTests.java | 223 +++++ .../bind/CollectionBinderTests.java | 264 ++++++ .../properties/bind/JavaBeanBinderTests.java | 810 ++++++++++++++++++ .../properties/bind/MapBinderTests.java | 355 ++++++++ ...pertySourcesPlaceholdersResolverTests.java | 106 +++ .../convert/AbstractInetAddressTests.java | 40 + .../convert/BinderConversionServiceTests.java | 162 ++++ .../InetAddressToStringConverterTests.java | 47 + .../convert/PropertyEditorConverterTests.java | 96 +++ .../ResolvableTypeDescriptorTests.java | 63 ++ .../StringToCharArrayConverterTests.java | 38 + .../StringToEnumConverterFactoryTests.java | 91 ++ .../StringToInetAddressConverterTests.java | 53 ++ .../handler/IgnoreErrorsBindHandlerTests.java | 85 ++ ...gnoreNestedPropertiesBindHandlerTests.java | 106 +++ .../NoUnboundElementsBindHandlerTests.java | 120 +++ .../test/PackagePrivateBeanBindingTests.java | 81 ++ .../BindValidationExceptionTests.java | 51 ++ .../OriginTrackedFieldErrorTests.java | 64 ++ .../ValidationBindHandlerTests.java | 249 ++++++ .../validation/ValidationErrorsTests.java | 124 +++ .../properties/bind/convert/resource.txt | 1 + 64 files changed, 7498 insertions(+) create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateElementBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindException.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolver.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/UnboundConfigurationPropertiesException.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptor.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/package-info.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/package-info.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldError.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/package-info.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolverTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptorTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandlerTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandlerTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/test/PackagePrivateBeanBindingTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/BindValidationExceptionTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldErrorTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationErrorsTests.java create mode 100644 spring-boot/src/test/resources/org/springframework/boot/context/properties/bind/convert/resource.txt diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java new file mode 100644 index 0000000000..e10177d816 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link BindHandler} implementations. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public abstract class AbstractBindHandler implements BindHandler { + + private final BindHandler parent; + + /** + * Create a new binding handler instance. + */ + public AbstractBindHandler() { + this(BindHandler.DEFAULT); + } + + /** + * Create a new binding handler instance with a specific parent. + * @param parent the parent handler + */ + public AbstractBindHandler(BindHandler parent) { + Assert.notNull(parent, "Parent must not be null"); + this.parent = parent; + } + + @Override + public boolean onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + return this.parent.onStart(name, target, context); + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + return this.parent.onSuccess(name, target, context, result); + } + + @Override + public Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + return this.parent.onFailure(name, target, context, error); + } + + @Override + public void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + this.parent.onFinish(name, target, context, result); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java new file mode 100644 index 0000000000..64a6842d71 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.function.Supplier; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.ResolvableType; + +/** + * Internal strategy used by {@link Binder} to bind aggregates (Maps, Lists, Arrays). + * + * @param the type being bound + * @author Phillip Webb + * @author Madhura Bhave + */ +abstract class AggregateBinder { + + private final BindContext context; + + AggregateBinder(BindContext context) { + this.context = context; + } + + /** + * Perform binding for the aggregate. + * @param name the configuration property name to bind + * @param target the target to bind + * @param itemBinder an item binder + * @return the bound aggregate or null + */ + @SuppressWarnings("unchecked") + public final Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder itemBinder) { + Supplier value = target.getValue(); + Class type = (value == null ? target.getType().resolve() + : ResolvableType.forClass(AggregateBinder.class, getClass()) + .resolveGeneric()); + Object result = bind(name, target, itemBinder, type); + if (result == null || value == null || value.get() == null) { + return result; + } + return merge((T) value.get(), (T) result); + } + + /** + * Perform the actual aggregate binding. + * @param name the configuration property name to bind + * @param target the target to bind + * @param elementBinder an element binder + * @param type the aggregate actual type to use + * @return the bound result + */ + protected abstract Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, Class type); + + /** + * Merge any additional elements into the existing aggregate. + * @param existing the existing value + * @param additional the additional elements to merge + * @return the merged result + */ + protected abstract T merge(T existing, T additional); + + /** + * Return the context being used by this binder. + * @return the context + */ + protected final BindContext getContext() { + return this.context; + } + + /** + * Roll up the given name to the first element below the root. For example a name of + * {@code foo.bar.baz} rolled up to the root {@code foo} would be {@code foo.bar}. + * @param name the name to roll up + * @param root the root name + * @return the rolled up name or {@code null} + */ + protected final ConfigurationPropertyName rollUp(ConfigurationPropertyName name, + ConfigurationPropertyName root) { + while (name != null && (name.getParent() != null) + && (!root.equals(name.getParent()))) { + name = name.getParent(); + } + return name; + } + + /** + * Internal class used to supply the aggregate and cache the value. + * @param The aggregate type + */ + protected static class AggregateSupplier { + + private final Supplier supplier; + + private T supplied; + + public AggregateSupplier(Supplier supplier) { + this.supplier = supplier; + } + + public T get() { + if (this.supplied == null) { + this.supplied = this.supplier.get(); + } + return this.supplied; + } + + public boolean wasSupplied() { + return this.supplied != null; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateElementBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateElementBinder.java new file mode 100644 index 0000000000..a8edc73f71 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateElementBinder.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; + +/** + * Binder that can be used by {@link AggregateBinder} implementations to recursively bind + * elements. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +@FunctionalInterface +interface AggregateElementBinder { + + /** + * Bind the given name to a target bindable. + * @param name the name to bind + * @param target the target bindable + * @return a bound object or {@code null} + */ + default Object bind(ConfigurationPropertyName name, Bindable target) { + return bind(name, target, null); + } + + /** + * Bind the given name to a target bindable using optionally limited to a single + * source. + * @param name the name to bind + * @param target the target bindable + * @param source the source of the elements or {@code null} to use all sources + * @return a bound object or {@code null} + */ + Object bind(ConfigurationPropertyName name, Bindable target, + ConfigurationPropertySource source); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java new file mode 100644 index 0000000000..eb981e2dbe --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.ResolvableType; + +/** + * {@link AggregateBinder} for arrays. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ArrayBinder extends IndexedElementsBinder { + + ArrayBinder(BindContext context) { + super(context); + } + + @Override + protected Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, Class type) { + IndexedCollectionSupplier collection = new IndexedCollectionSupplier( + ArrayList::new); + ResolvableType elementType = target.getType().getComponentType(); + bindIndexed(name, target, elementBinder, collection, target.getType(), + elementType); + if (collection.wasSupplied()) { + List list = (List) collection.get(); + Object array = Array.newInstance(elementType.resolve(), list.size()); + for (int i = 0; i < list.size(); i++) { + Array.set(array, i, list.get(i)); + } + return array; + } + return null; + } + + @Override + protected Object merge(Object existing, Object additional) { + return additional; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java new file mode 100644 index 0000000000..c1889f800c --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +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 The source type + * @return a bound instance or {@code null} + */ + T bind(Bindable target, boolean hasKnownBindableProperties, + BeanPropertyBinder propertyBinder); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java new file mode 100644 index 0000000000..e8e059e980 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +/** + * 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); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java new file mode 100644 index 0000000000..068c4ccf00 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +/** + * 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(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java new file mode 100644 index 0000000000..b4f0001c3c --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.core.convert.ConversionService; + +/** + * Context information for use by {@link BindHandler BindHandlers}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public interface BindContext { + + /** + * Return the current depth of the binding. Root binding starts with a depth of + * {@code 0}. Each subsequent property binding increases the depth by {@code 1}. + * @return the depth of the current binding + */ + int getDepth(); + + /** + * Return the {@link ConfigurationPropertySource sources} being used by the + * {@link Binder}. + * @return the sources + */ + Iterable getSources(); + + /** + * Return the {@link ConfigurationProperty} actually being bound or {@code null} if + * the property has not yet been determined. + * @return the configuration property (may be {@code null}). + */ + ConfigurationProperty getConfigurationProperty(); + + /** + * Return the {@link PlaceholdersResolver} being used by the binder. + * @return the {@link PlaceholdersResolver} (never {@code null}) + */ + PlaceholdersResolver getPlaceholdersResolver(); + + /** + * Return the {@link ConversionService} used by the binder. + * @return the conversion service + */ + BinderConversionService getConversionService(); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindException.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindException.java new file mode 100644 index 0000000000..ddf5d4ba62 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindException.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; + +/** + * Exception thrown when binding fails. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class BindException extends RuntimeException implements OriginProvider { + + private final Bindable target; + + private final ConfigurationProperty property; + + private final ConfigurationPropertyName name; + + BindException(ConfigurationPropertyName name, Bindable target, + ConfigurationProperty property, Throwable cause) { + super(buildMessage(name, target), cause); + this.name = name; + this.target = target; + this.property = property; + } + + /** + * Return the name of the configuration property being bound. + * @return the configuration property name + */ + public ConfigurationPropertyName getName() { + return this.name; + } + + /** + * Return the target being bound. + * @return the bind target + */ + public Bindable getTarget() { + return this.target; + } + + /** + * Return the configuration property name of the item that was being bound. + * @return the configuration property name + */ + public ConfigurationProperty getProperty() { + return this.property; + } + + @Override + public Origin getOrigin() { + return Origin.from(this.name); + } + + private static String buildMessage(ConfigurationPropertyName name, + Bindable target) { + StringBuilder message = new StringBuilder(); + message.append("Failed to bind properties"); + message.append(name == null ? "" : " under '" + name + "'"); + message.append(" to " + target.getType()); + return message.toString(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java new file mode 100644 index 0000000000..b564b8eb37 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; + +/** + * Callback interface that can be used to handle additional logic during element + * {@link Binder binding}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public interface BindHandler { + + /** + * Default no-op bind hander. + */ + BindHandler DEFAULT = new BindHandler() { + + }; + + /** + * Called when binding of an element starts but before any result has been determined. + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @return {@code true} if binding should proceed + */ + default boolean onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + return true; + } + + /** + * Called when binding of an element ends with a successful result. Implementations + * may change the ultimately returned result or perform addition validation. + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @param result the bound result (never {@code null}) + * @return the actual result that should be used (may be {@code null}) + */ + default Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + return result; + } + + /** + * Called when binding fails for any reason (including failures from + * {@link #onSuccess} calls). Implementations may chose to swallow exceptions and + * return an alternative result. + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @param error the cause of the error (if the exception stands it may be re-thrown) + * @return the actual result that should be used (may be {@code null}). + * @throws Exception if the binding isn't valid + */ + default Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + throw error; + } + + /** + * Called when binding finishes, regardless of whether the property was bound or not. + * @param name the name of the element being bound + * @param target the item being bound + * @param context the bind context + * @param result the bound result (may be {@code null}) + * @throws Exception if the binding isn't valid + */ + default void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java new file mode 100644 index 0000000000..72754f7bc3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java @@ -0,0 +1,167 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.NoSuchElementException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A container object to return result of a {@link Binder} bind operation. May contain + * either a successfully bound object or an empty result. + * + * @param The result type + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public final class BindResult { + + private static final BindResult UNBOUND = new BindResult<>(null); + + private final T value; + + private BindResult(T value) { + this.value = value; + } + + /** + * Return the object that was bound or throw a {@link NoSuchElementException} if no + * value was bound. + * @return the the bound value (never {@code null}) + * @throws NoSuchElementException if no value was bound + * @see #isBound() + */ + public T get() throws NoSuchElementException { + if (this.value == null) { + throw new NoSuchElementException("No value bound"); + } + return this.value; + } + + /** + * Returns {@code true} if a result was bound. + * @return if a result was bound + */ + public boolean isBound() { + return (this.value != null); + } + + /** + * Invoke the specified consumer with the bound value, or do nothing if no value has + * been bound. + * @param consumer block to execute if a value has been bound + */ + public void ifBound(Consumer consumer) { + Assert.notNull(consumer, "Consumer must not be null"); + if (this.value != null) { + consumer.accept(this.value); + } + } + + /** + * Apply the provided mapping function to the bound value, or return an updated + * unbound result if no value has been bound. + * @param The type of the result of the mapping function + * @param mapper a mapping function to apply to the bound value. The mapper will not + * be invoked if no value has been bound. + * @return an {@code BindResult} describing the result of applying a mapping function + * to the value of this {@code BindResult}. + */ + public BindResult map(Function mapper) { + Assert.notNull(mapper, "Mapper must not be null"); + return of(this.value == null ? null : mapper.apply(this.value)); + } + + /** + * Return the object that was bound, or {@code other} if no value has been bound. + * @param other the value to be returned if there is no bound value (may be + * {@code null}) + * @return the value, if bound, otherwise {@code other} + */ + public T orElse(T other) { + return (this.value != null ? this.value : other); + } + + /** + * Return the object that was bound, or the result of invoking {@code other} if no + * value has been bound. + * @param other a {@link Supplier} of the value to be returned if there is no bound + * value + * @return the value, if bound, otherwise the supplied {@code other} + */ + public T orElseGet(Supplier other) { + return (this.value != null ? this.value : other.get()); + } + + /** + * Return the object that was bound, or a new instance of the specified class if no + * value has been bound. + * @param type the type to create if no value was bound + * @return the value, if bound, otherwise a new instance of {@code type} + */ + public T orElseCreate(Class type) { + Assert.notNull(type, "Type must not be null"); + return (this.value != null ? this.value : BeanUtils.instantiateClass(type)); + } + + /** + * Return the object that was bound, or throw an exception to be created by the + * provided supplier if no value has been bound. + * @param Type of the exception to be thrown + * @param exceptionSupplier The supplier which will return the exception to be thrown + * @return the present value + * @throws X if there is no value present + */ + public T orElseThrow(Supplier exceptionSupplier) + throws X { + if (this.value == null) { + throw exceptionSupplier.get(); + } + return this.value; + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return ObjectUtils.nullSafeEquals(this.value, ((BindResult) obj).value); + } + + @SuppressWarnings("unchecked") + static BindResult of(T value) { + if (value == null) { + return (BindResult) UNBOUND; + } + return new BindResult(value); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java new file mode 100644 index 0000000000..14ea640886 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java @@ -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 The source type + * @author Philip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see Bindable#of(Class) + * @see Bindable#of(ResolvableType) + */ +public final class Bindable { + + private static final Annotation[] NO_ANNOTATIONS = {}; + + private final ResolvableType type; + + private final ResolvableType boxedType; + + private final Supplier value; + + private final Annotation[] annotations; + + private Bindable(ResolvableType type, ResolvableType boxedType, Supplier value, + Annotation[] annotations) { + this.type = type; + this.boxedType = boxedType; + this.value = value; + this.annotations = annotations; + } + + /** + * Return the type of the item to bind. + * @return the type being bound + */ + public ResolvableType getType() { + return this.type; + } + + public ResolvableType getBoxedType() { + return this.boxedType; + } + + /** + * Return a supplier that provides the object value or {@code null}. + * @return the value or {@code null} + */ + public Supplier getValue() { + return this.value; + } + + /** + * Return any associated annotations that could affect binding. + * @return the associated annotations + */ + public Annotation[] getAnnotations() { + return this.annotations; + } + + @Override + public String toString() { + ToStringCreator creator = new ToStringCreator(this); + creator.append("type", this.type); + creator.append("value", (this.value == null ? "none" : "provided")); + creator.append("annotations", this.annotations); + return creator.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ObjectUtils.nullSafeHashCode(this.type); + result = prime * result + ObjectUtils.nullSafeHashCode(this.annotations); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Bindable other = (Bindable) obj; + boolean result = true; + result = result && nullSafeEquals(this.type.resolve(), other.type.resolve()); + result = result && nullSafeEquals(this.annotations, other.annotations); + return result; + } + + private boolean nullSafeEquals(Object o1, Object o2) { + return ObjectUtils.nullSafeEquals(o1, o2); + } + + /** + * Create an updated {@link Bindable} instance with the specified annotations. + * @param annotations the annotations + * @return an updated {@link Bindable} + */ + public Bindable withAnnotations(Annotation... annotations) { + return new Bindable(this.type, this.boxedType, this.value, + (annotations == null ? NO_ANNOTATIONS : annotations)); + } + + public Bindable withExistingValue(T existingValue) { + Assert.isTrue( + existingValue == null || this.type.isArray() + || this.boxedType.resolve().isInstance(existingValue), + "ExistingValue must be an instance of " + this.type); + Supplier value = (existingValue == null ? null : () -> existingValue); + return new Bindable<>(this.type, this.boxedType, value, NO_ANNOTATIONS); + } + + public Bindable withSuppliedValue(Supplier suppliedValue) { + return new Bindable<>(this.type, this.boxedType, suppliedValue, NO_ANNOTATIONS); + } + + /** + * Create a new {@link Bindable} of the type of the specified instance with an + * existing value equal to the instance. + * @param The source type + * @param instance the instance (must not be {@code null}) + * @return a {@link Bindable} instance + * @see #of(ResolvableType) + * @see #withExistingValue(Object) + */ + @SuppressWarnings("unchecked") + public static Bindable ofInstance(T instance) { + Assert.notNull(instance, "Instance must not be null"); + Class type = (Class) instance.getClass(); + return of(type).withExistingValue(instance); + } + + /** + * Create a new {@link Bindable} of the specified type. + * @param The source type + * @param type the type (must not be {@code null}) + * @return a {@link Bindable} instance + * @see #of(ResolvableType) + */ + public static Bindable of(Class type) { + Assert.notNull(type, "Type must not be null"); + return of(ResolvableType.forClass(type)); + } + + /** + * Create a new {@link Bindable} {@link List} of the specified element type. + * @param the element type + * @param elementType the list element type + * @return a {@link Bindable} instance + */ + public static Bindable> listOf(Class elementType) { + return of(ResolvableType.forClassWithGenerics(List.class, elementType)); + } + + /** + * Create a new {@link Bindable} {@link Set} of the specified element type. + * @param the element type + * @param elementType the set element type + * @return a {@link Bindable} instance + */ + public static Bindable> setOf(Class elementType) { + return of(ResolvableType.forClassWithGenerics(Set.class, elementType)); + } + + /** + * Create a new {@link Bindable} {@link Map} of the specified kay and value type. + * @param the key type + * @param the value type + * @param keyType the map key type + * @param valueType the map value type + * @return a {@link Bindable} instance + */ + public static Bindable> mapOf(Class keyType, Class valueType) { + return of(ResolvableType.forClassWithGenerics(Map.class, keyType, valueType)); + } + + /** + * Create a new {@link Bindable} of the specified type. + * @param The source type + * @param type the type (must not be {@code null}) + * @return a {@link Bindable} instance + * @see #of(Class) + */ + public static Bindable of(ResolvableType type) { + Assert.notNull(type, "Type must not be null"); + ResolvableType boxedType = box(type); + return new Bindable<>(type, boxedType, null, NO_ANNOTATIONS); + } + + private static ResolvableType box(ResolvableType type) { + Class resolved = type.resolve(); + if (resolved != null && resolved.isPrimitive()) { + Object array = Array.newInstance(resolved, 1); + Class wrapperType = Array.get(array, 0).getClass(); + return ResolvableType.forClass(wrapperType); + } + if (resolved.isArray()) { + return ResolvableType.forArrayComponent(box(type.getComponentType())); + } + return type; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java new file mode 100644 index 0000000000..e31dcb840b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -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> NON_BEAN_CLASSES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(Object.class, Class.class))); + + private static final List BEAN_BINDERS; + + static { + List beanBinders = new ArrayList<>(); + beanBinders.add(new JavaBeanBinder()); + BEAN_BINDERS = Collections.unmodifiableList(beanBinders); + } + + private final Iterable sources; + + private final PlaceholdersResolver placeholdersResolver; + + private final BinderConversionService conversionService; + + /** + * Create a new {@link Binder} instance for the specified sources. A + * {@link DefaultFormattingConversionService} will be used for all conversion. + * @param sources the sources used for binding + */ + public Binder(ConfigurationPropertySource... sources) { + this(Arrays.asList(sources), null, null); + } + + /** + * Create a new {@link Binder} instance for the specified sources. A + * {@link DefaultFormattingConversionService} will be used for all conversion. + * @param sources the sources used for binding + */ + public Binder(Iterable sources) { + this(sources, null, null); + } + + /** + * Create a new {@link Binder} instance for the specified sources. + * @param sources the sources used for binding + * @param placeholdersResolver strategy to resolve any property place-holders + */ + public Binder(Iterable sources, + PlaceholdersResolver placeholdersResolver) { + this(sources, placeholdersResolver, null); + } + + /** + * Create a new {@link Binder} instance for the specified sources. + * @param sources the sources used for binding + * @param placeholdersResolver strategy to resolve any property place-holders + * @param conversionService the conversion service to convert values + */ + public Binder(Iterable sources, + PlaceholdersResolver placeholdersResolver, + ConversionService conversionService) { + Assert.notNull(sources, "Sources must not be null"); + this.sources = sources; + this.placeholdersResolver = (placeholdersResolver != null ? placeholdersResolver + : PlaceholdersResolver.NONE); + this.conversionService = (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 the bound type + * @return the binding result (never {@code null}) + * @see #bind(ConfigurationPropertyName, Bindable, BindHandler) + */ + public BindResult bind(String name, Class target) { + return bind(name, Bindable.of(target)); + } + + /** + * Bind the specified target {@link Bindable} using this binders + * {@link ConfigurationPropertySource property sources}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param the bound type + * @return the binding result (never {@code null}) + * @see #bind(ConfigurationPropertyName, Bindable, BindHandler) + */ + public BindResult bind(String name, Bindable target) { + return bind(ConfigurationPropertyName.of(name), target, null); + } + + /** + * Bind the specified target {@link Bindable} using this binders + * {@link ConfigurationPropertySource property sources}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param the bound type + * @return the binding result (never {@code null}) + * @see #bind(ConfigurationPropertyName, Bindable, BindHandler) + */ + public BindResult bind(ConfigurationPropertyName name, Bindable target) { + return bind(name, target, null); + } + + /** + * Bind the specified target {@link Bindable} using this binders + * {@link ConfigurationPropertySource property sources}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param handler the bind handler (may be {@code null}) + * @param the bound type + * @return the binding result (never {@code null}) + */ + public BindResult bind(String name, Bindable target, BindHandler handler) { + return bind(ConfigurationPropertyName.of(name), target, handler); + } + + /** + * Bind the specified target {@link Bindable} using this binders + * {@link ConfigurationPropertySource property sources}. + * @param name the configuration property name to bind + * @param target the target bindable + * @param handler the bind handler (may be {@code null}) + * @param the bound type + * @return the binding result (never {@code null}) + */ + public BindResult bind(ConfigurationPropertyName name, Bindable target, + BindHandler handler) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(target, "Target must not be null"); + handler = (handler != null ? handler : BindHandler.DEFAULT); + Context context = new Context(); + T bound = bind(name, target, handler, context); + return BindResult.of(bound); + } + + protected final T bind(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context) { + try { + if (!handler.onStart(name, target, context)) { + return null; + } + Object bound = bindObject(name, target, handler, context); + return handleBindResult(name, target, handler, context, bound); + } + catch (Exception ex) { + return handleBindError(name, target, handler, context, ex); + } + } + + private T handleBindResult(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, Object result) throws Exception { + result = convert(result, target); + if (result != null) { + result = handler.onSuccess(name, target, context, result); + result = convert(result, target); + } + handler.onFinish(name, target, context, result); + return convert(result, target); + } + + private T handleBindError(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, Exception error) { + try { + Object result = handler.onFailure(name, target, context, error); + return convert(result, target); + } + catch (Exception ex) { + if (ex instanceof BindException) { + throw (BindException) ex; + } + throw new BindException(name, target, context.getConfigurationProperty(), ex); + } + } + + private T convert(Object value, Bindable target) { + if (value == null) { + return null; + } + return this.conversionService.convert(value, target); + } + + private Object bindObject(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context) throws Exception { + AggregateBinder aggregateBinder = getAggregateBinder(target, context); + if (aggregateBinder != null) { + return bindAggregate(name, target, handler, context, aggregateBinder); + } + ConfigurationProperty property = findProperty(name, 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 Object bindAggregate(ConfigurationPropertyName name, Bindable 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 Object bindProperty(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context, ConfigurationProperty property) { + context.setConfigurationProperty(property); + Object result = property.getValue(); + result = this.placeholdersResolver.resolvePlaceholders(result); + result = this.conversionService.convert(result, target); + return result; + } + + private Object bindBean(ConfigurationPropertyName name, Bindable target, + BindHandler handler, Context context) throws Exception { + 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 source = Arrays + .asList((ConfigurationPropertySource) null); + + private final Deque> beans = new ArrayDeque<>(); + + private ConfigurationProperty configurationProperty; + + void increaseDepth() { + this.depth++; + } + + void decreaseDepth() { + this.depth--; + } + + @Override + public int getDepth() { + return this.depth; + } + + public T withSource(ConfigurationPropertySource source, + Supplier supplier) { + if (source == null) { + return supplier.get(); + } + this.source.set(0, source); + this.sourcePushCount++; + try { + return supplier.get(); + } + finally { + this.sourcePushCount--; + } + } + + public T withBean(Class bean, Supplier supplier) { + this.beans.push(bean); + try { + return withIncreasedDepth(supplier); + } + finally { + this.beans.pop(); + } + } + + public T withIncreasedDepth(Supplier supplier) { + increaseDepth(); + try { + return supplier.get(); + } + finally { + decreaseDepth(); + } + } + + private Stream streamSources() { + if (this.sourcePushCount > 0) { + return this.source.stream(); + } + return StreamSupport.stream(Binder.this.sources.spliterator(), false); + } + + @Override + public Iterable 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; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java new file mode 100644 index 0000000000..96aa7885b3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.Collection; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; + +/** + * {@link AggregateBinder} for collections. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class CollectionBinder extends IndexedElementsBinder> { + + CollectionBinder(BindContext context) { + super(context); + } + + @Override + protected Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, Class type) { + IndexedCollectionSupplier collection = new IndexedCollectionSupplier( + () -> CollectionFactory.createCollection(type, 0)); + ResolvableType elementType = target.getType().asCollection().getGeneric(); + bindIndexed(name, target, elementBinder, collection, target.getType(), + elementType); + if (collection.wasSupplied()) { + return collection.get(); + } + return null; + } + + @Override + protected Collection merge(Collection existing, + Collection additional) { + existing.clear(); + existing.addAll(additional); + return existing; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java new file mode 100644 index 0000000000..12a0fcd1e8 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.Collection; +import java.util.List; +import java.util.TreeSet; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.core.ResolvableType; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Base class for {@link AggregateBinder AggregateBinders} that read a sequential run of + * indexed items. + * + * @param the type being bound + * @author Phillip Webb + * @author Madhura Bhave + */ +abstract class IndexedElementsBinder extends AggregateBinder { + + IndexedElementsBinder(BindContext context) { + super(context); + } + + protected final void bindIndexed(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, IndexedCollectionSupplier collection, + ResolvableType aggregateType, ResolvableType elementType) { + for (ConfigurationPropertySource source : getContext().getSources()) { + bindIndexed(source, name, elementBinder, collection, aggregateType, + elementType); + if (collection.wasSupplied() && collection.get() != null) { + return; + } + } + } + + private void bindIndexed(ConfigurationPropertySource source, + ConfigurationPropertyName root, AggregateElementBinder elementBinder, + IndexedCollectionSupplier collection, ResolvableType aggregateType, + ResolvableType elementType) { + ConfigurationProperty property = source.getConfigurationProperty(root); + if (property != null) { + Object aggregate = convert(property.getValue(), aggregateType); + ResolvableType collectionType = ResolvableType + .forClassWithGenerics(collection.get().getClass(), elementType); + Collection elements = convert(aggregate, collectionType); + collection.get().addAll(elements); + } + else { + bindIndexed(source, root, elementBinder, collection, elementType); + } + } + + private void bindIndexed(ConfigurationPropertySource source, + ConfigurationPropertyName root, AggregateElementBinder elementBinder, + IndexedCollectionSupplier collection, ResolvableType elementType) { + MultiValueMap knownIndexedChildren = getKnownIndexedChildren( + source, root); + for (int i = 0; i < Integer.MAX_VALUE; i++) { + ConfigurationPropertyName name = root.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 getKnownIndexedChildren( + ConfigurationPropertySource source, ConfigurationPropertyName root) { + MultiValueMap children = new LinkedMultiValueMap<>(); + for (ConfigurationPropertyName name : source.filter(root::isAncestorOf)) { + name = rollUp(name, root); + if (name.getElement().isIndexed()) { + String key = name.getElement().getValue(Form.UNIFORM); + ConfigurationProperty value = source.getConfigurationProperty(name); + children.add(key, value); + } + } + return children; + } + + private void assertNoUnboundChildren( + MultiValueMap children) { + if (!children.isEmpty()) { + throw new UnboundConfigurationPropertiesException( + children.values().stream().flatMap(List::stream) + .collect(Collectors.toCollection(TreeSet::new))); + } + } + + @SuppressWarnings("unchecked") + private C convert(Object value, ResolvableType type) { + value = getContext().getPlaceholdersResolver().resolvePlaceholders(value); + BinderConversionService conversionService = getContext().getConversionService(); + return (C) conversionService.convert(value, type); + } + + /** + * {@link AggregateBinder.AggregateSupplier AggregateSupplier} for an index + * collection. + */ + protected static class IndexedCollectionSupplier + extends AggregateSupplier> { + + public IndexedCollectionSupplier(Supplier> supplier) { + super(supplier); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java new file mode 100644 index 0000000000..adf0382398 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java @@ -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 bind(Bindable target, boolean hasKnownBindableProperties, + BeanPropertyBinder propertyBinder) { + Bean bean = Bean.get(target, hasKnownBindableProperties); + if (bean == null) { + return null; + } + BeanSupplier beanSupplier = bean.getSupplier(target); + boolean bound = bind(target, propertyBinder, bean, beanSupplier); + return (bound ? beanSupplier.get() : null); + } + + private boolean bind(Bindable target, BeanPropertyBinder propertyBinder, + Bean bean, BeanSupplier beanSupplier) { + boolean bound = false; + for (Map.Entry entry : bean.getProperties().entrySet()) { + bound |= bind(beanSupplier, propertyBinder, entry.getValue()); + } + return bound; + } + + private boolean bind(BeanSupplier beanSupplier, + BeanPropertyBinder propertyBinder, BeanProperty property) { + String propertyName = property.getName(); + ResolvableType type = property.getType(); + Supplier 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 { + + private static Bean cached; + + private final Class type; + + private final Map 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 getProperties() { + return this.properties; + } + + @SuppressWarnings("unchecked") + public BeanSupplier getSupplier(Bindable target) { + return new BeanSupplier(() -> { + 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 Bean get(Bindable bindable, + boolean useExistingValueForType) { + Class type = bindable.getType().resolve(); + Supplier 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) 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 implements Supplier { + + private final Supplier factory; + + private T instance; + + BeanSupplier(Supplier 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 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); + } + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java new file mode 100644 index 0000000000..37420371e4 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java @@ -0,0 +1,146 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; + +/** + * {@link AggregateBinder} for Maps. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class MapBinder extends AggregateBinder> { + + MapBinder(BindContext context) { + super(context); + } + + @Override + protected Object bind(ConfigurationPropertyName name, Bindable target, + AggregateElementBinder elementBinder, Class type) { + Map map = CollectionFactory.createMap(type, 0); + for (ConfigurationPropertySource source : getContext().getSources()) { + if (!ConfigurationPropertyName.EMPTY.equals(name)) { + source = source.filter(name::isAncestorOf); + } + new EntryBinder(name, target, elementBinder).bindEntries(source, map); + } + return (map.isEmpty() ? null : map); + } + + @Override + protected Map merge(Map existing, + Map additional) { + existing.putAll(additional); + return existing; + } + + private class EntryBinder { + + private final ConfigurationPropertyName root; + + private final AggregateElementBinder elementBinder; + + private final ResolvableType mapType; + + private final ResolvableType keyType; + + private final ResolvableType valueType; + + EntryBinder(ConfigurationPropertyName root, Bindable target, + AggregateElementBinder elementBinder) { + this.root = root; + this.elementBinder = elementBinder; + this.mapType = target.getType().asMap(); + this.keyType = this.mapType.getGeneric(0); + this.valueType = this.mapType.getGeneric(1); + } + + public void bindEntries(ConfigurationPropertySource source, + Map map) { + for (ConfigurationPropertyName name : source) { + Bindable valueBindable = getValueBindable(source, name); + ConfigurationPropertyName entryName = getEntryName(source, name); + Object key = getContext().getConversionService() + .convert(getKeyName(entryName), this.keyType); + Object value = this.elementBinder.bind(entryName, valueBindable); + map.putIfAbsent(key, value); + } + } + + private Bindable getValueBindable(ConfigurationPropertySource source, + ConfigurationPropertyName name) { + if (isMultiElementName(name) && isValueTreatedAsNestedMap()) { + return Bindable.of(this.mapType); + } + return Bindable.of(this.valueType); + } + + private ConfigurationPropertyName getEntryName(ConfigurationPropertySource source, + ConfigurationPropertyName name) { + if (isMultiElementName(name) + && (isValueTreatedAsNestedMap() || !isScalarValue(source, name))) { + return rollUp(name, this.root); + } + return name; + } + + private boolean isMultiElementName(ConfigurationPropertyName name) { + return name.getParent() != null && !this.root.equals(name.getParent()); + } + + private boolean isValueTreatedAsNestedMap() { + return Object.class.equals(this.valueType.resolve(Object.class)); + } + + private boolean isScalarValue(ConfigurationPropertySource source, + ConfigurationPropertyName name) { + if (Map.class.isAssignableFrom(this.valueType.resolve()) + || Collection.class.isAssignableFrom(this.valueType.resolve()) + || this.valueType.isArray()) { + return false; + } + ConfigurationProperty property = source.getConfigurationProperty(name); + if (property == null) { + return false; + } + Object value = property.getValue(); + value = getContext().getPlaceholdersResolver().resolvePlaceholders(value); + BinderConversionService conversionService = getContext() + .getConversionService(); + return conversionService.canConvert(value, this.valueType); + } + + private String getKeyName(ConfigurationPropertyName name) { + return name.stream(this.root).map((e) -> e.getValue(Form.ORIGINAL)) + .collect(Collectors.joining(".")); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java new file mode 100644 index 0000000000..5bc2900d58 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import org.springframework.core.env.PropertyResolver; + +/** + * Optional strategy that used by a {@link Binder} to resolve property placeholders. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see PropertySourcesPlaceholdersResolver + */ +@FunctionalInterface +public interface PlaceholdersResolver { + + /** + * No-op {@link PropertyResolver}. + */ + PlaceholdersResolver NONE = (value) -> value; + + /** + * Called to resolve any place holders in the given value. + * @param value the source value + * @return a value with place holders resolved + */ + Object resolvePlaceholders(Object value); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolver.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolver.java new file mode 100644 index 0000000000..a168d651eb --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolver.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySources; +import org.springframework.util.Assert; +import org.springframework.util.PropertyPlaceholderHelper; +import org.springframework.util.SystemPropertyUtils; + +/** + * {@link PlaceholdersResolver} to resolve placeholders from {@link PropertySources}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class PropertySourcesPlaceholdersResolver implements PlaceholdersResolver { + + private PropertySources sources; + + private PropertyPlaceholderHelper helper; + + public PropertySourcesPlaceholdersResolver(Environment environment) { + this(getSources(environment), null); + } + + public PropertySourcesPlaceholdersResolver(PropertySources sources) { + this(sources, null); + } + + public PropertySourcesPlaceholdersResolver(PropertySources sources, + PropertyPlaceholderHelper helper) { + this.sources = sources; + this.helper = (helper != null ? helper + : new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, + SystemPropertyUtils.PLACEHOLDER_SUFFIX, + SystemPropertyUtils.VALUE_SEPARATOR, false)); + } + + @Override + public Object resolvePlaceholders(Object value) { + if (value != null && value instanceof String) { + return this.helper.replacePlaceholders((String) value, + this::resolvePlaceholder); + } + return value; + } + + private String resolvePlaceholder(String placeholder) { + if (this.sources != null) { + for (PropertySource source : this.sources) { + Object value = source.getProperty(placeholder); + if (value != null) { + return String.valueOf(value); + } + } + } + return null; + } + + private static PropertySources getSources(Environment environment) { + Assert.notNull(environment, "Environment must not be null"); + Assert.isInstanceOf(ConfigurableEnvironment.class, environment, + "Environment must be a ConfigurableEnvironment"); + return ((ConfigurableEnvironment) environment).getPropertySources(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/UnboundConfigurationPropertiesException.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/UnboundConfigurationPropertiesException.java new file mode 100644 index 0000000000..62b961ecc3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/UnboundConfigurationPropertiesException.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; + +/** + * {@link BindException} thrown when {@link ConfigurationPropertySource} elements were + * left unbound. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class UnboundConfigurationPropertiesException extends RuntimeException { + + private final Set unboundProperties; + + public UnboundConfigurationPropertiesException( + Set unboundProperties) { + super(buildMessage(unboundProperties)); + this.unboundProperties = Collections.unmodifiableSet(unboundProperties); + } + + public Set getUnboundProperties() { + return this.unboundProperties; + } + + private static String buildMessage(Set unboundProperties) { + StringBuilder builder = new StringBuilder(); + builder.append("The elements ["); + String message = unboundProperties.stream().map((p) -> p.getName().toString()) + .collect(Collectors.joining(",")); + builder.append(message).append("] were left unbound."); + return builder.toString(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java new file mode 100644 index 0000000000..9179579e78 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java @@ -0,0 +1,147 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.util.function.Function; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.datetime.DateFormatter; +import org.springframework.format.datetime.DateFormatterRegistrar; +import org.springframework.format.support.DefaultFormattingConversionService; + +/** + * Internal {@link ConversionService} used by the {@link Binder}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 2.0.0 + */ +public class BinderConversionService implements ConversionService { + + private final ConversionService conversionService; + + private final ConversionService additionalConversionService; + + /** + * Create a new {@link BinderConversionService} instance. + * @param conversionService and option root conversion service + */ + public BinderConversionService(ConversionService conversionService) { + this.conversionService = (conversionService != null ? conversionService + : new DefaultFormattingConversionService()); + this.additionalConversionService = createAdditionalConversionService(); + } + + /** + * Return {@code true} if the given source object can be converted to the + * {@code targetType}. + * @param source the source object + * @param targetType the target type to convert to (required) + * @return {@code true} if a conversion can be performed, {@code false} if not + * @throws IllegalArgumentException if {@code targetType} is {@code null} + */ + public boolean canConvert(Object source, ResolvableType targetType) { + TypeDescriptor sourceType = TypeDescriptor.forObject(source); + return canConvert(sourceType, ResolvableTypeDescriptor.forType(targetType)); + } + + @Override + public boolean canConvert(Class sourceType, Class targetType) { + return (this.conversionService != null + && this.conversionService.canConvert(sourceType, targetType)) + || this.additionalConversionService.canConvert(sourceType, targetType); + } + + @Override + public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (this.conversionService != null + && this.conversionService.canConvert(sourceType, targetType)) + || this.additionalConversionService.canConvert(sourceType, targetType); + } + + @SuppressWarnings("unchecked") + public T convert(Object value, ResolvableType type) { + TypeDescriptor sourceType = TypeDescriptor.forObject(value); + TypeDescriptor targetType = ResolvableTypeDescriptor.forType(type); + return (T) convert(value, sourceType, targetType); + } + + @SuppressWarnings("unchecked") + public T convert(Object value, Bindable bindable) { + TypeDescriptor sourceType = TypeDescriptor.forObject(value); + TypeDescriptor targetType = ResolvableTypeDescriptor.forBindable(bindable); + return (T) convert(value, sourceType, targetType); + } + + @Override + public T convert(Object source, Class targetType) { + return callConversionService((c) -> c.convert(source, targetType)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return callConversionService((c) -> c.convert(source, sourceType, targetType)); + } + + private T callConversionService(Function call) { + if (this.conversionService == null) { + return callAdditionalConversionService(call, null); + } + try { + return call.apply(this.conversionService); + } + catch (ConversionException ex) { + return callAdditionalConversionService(call, ex); + } + } + + private T callAdditionalConversionService(Function call, + RuntimeException cause) { + try { + return call.apply(this.additionalConversionService); + } + catch (ConverterNotFoundException ex) { + throw (cause != null ? cause : ex); + } + } + + private static ConversionService createAdditionalConversionService() { + DefaultFormattingConversionService service = new DefaultFormattingConversionService(); + DefaultConversionService.addCollectionConverters(service); + service.addConverterFactory(new StringToEnumConverterFactory()); + service.addConverter(new StringToCharArrayConverter()); + service.addConverter(new StringToInetAddressConverter()); + service.addConverter(new InetAddressToStringConverter()); + service.addConverter(new PropertyEditorConverter()); + DateFormatterRegistrar registrar = new DateFormatterRegistrar(); + DateFormatter formatter = new DateFormatter(); + formatter.setIso(DateTimeFormat.ISO.DATE_TIME); + registrar.setFormatter(formatter); + registrar.registerFormatters(service); + return service; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java new file mode 100644 index 0000000000..02a09e37ad --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.beans.PropertyEditor; +import java.net.InetAddress; + +import org.springframework.core.convert.converter.Converter; + +/** + * {@link PropertyEditor} for {@link InetAddress} objects. + * + * @author Dave Syer + * @author Phillip Webb + */ +class InetAddressToStringConverter implements Converter { + + @Override + public String convert(InetAddress source) { + return source.getHostAddress(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java new file mode 100644 index 0000000000..977336eb72 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.beans.PropertyEditor; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.PropertyEditorRegistrySupport; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * {@link GenericConverter} that delegates to Java bean {@link PropertyEditor + * PropertyEditors}. + * + * @author Phillip Webb + */ +class PropertyEditorConverter implements GenericConverter, ConditionalConverter { + + private static final Set> SKIPPED; + + static { + Set> skipped = new LinkedHashSet<>(); + skipped.add(Collection.class); + skipped.add(Map.class); + SKIPPED = Collections.unmodifiableSet(skipped); + } + + /** + * Registry that can be used to check if conversion is supported. Since + * {@link PropertyEditor PropertyEditors} are not thread safe this can't be used for + * actual conversion. + */ + private final PropertyEditorRegistrySupport registry = new SimpleTypeConverter(); + + @Override + public Set getConvertibleTypes() { + return null; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + Class type = targetType.getType(); + if (isSkipped(type)) { + return false; + } + PropertyEditor editor = this.registry.getDefaultEditor(type); + editor = (editor != null ? editor : BeanUtils.findEditorByConvention(type)); + return editor != null; + } + + private boolean isSkipped(Class type) { + return SKIPPED.stream().anyMatch((c) -> c.isAssignableFrom(type)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return new SimpleTypeConverter().convertIfNecessary(source, targetType.getType()); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptor.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptor.java new file mode 100644 index 0000000000..76fe621b91 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.lang.annotation.Annotation; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.TypeDescriptor; + +/** + * A {@link TypeDescriptor} backed by a {@link ResolvableType}. + * + * @author Phillip Webb + */ +@SuppressWarnings("serial") +final class ResolvableTypeDescriptor extends TypeDescriptor { + + private ResolvableTypeDescriptor(ResolvableType resolvableType, + Annotation[] annotations) { + super(resolvableType, null, annotations); + } + + /** + * Create a {@link TypeDescriptor} for the specified {@link Bindable}. + * @param bindable the bindable + * @return the type descriptor + */ + public static TypeDescriptor forBindable(Bindable bindable) { + return forType(bindable.getType(), bindable.getAnnotations()); + } + + /** + * Return a {@link TypeDescriptor} for the specified {@link ResolvableType}. + * @param type the resolvable type + * @param annotations the annotations to include + * @return the type descriptor + */ + public static TypeDescriptor forType(ResolvableType type, Annotation... annotations) { + return new ResolvableTypeDescriptor(type, annotations); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java new file mode 100644 index 0000000000..67fbd690e9 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import org.springframework.core.convert.converter.Converter; + +/** + * Converts a String to a Char Array. + * + * @author Phillip Webb + */ +class StringToCharArrayConverter implements Converter { + + @Override + public char[] convert(String source) { + return source.toCharArray(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java new file mode 100644 index 0000000000..da9b4ab513 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.util.EnumSet; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.util.Assert; + +/** + * Converts from a String to a {@link java.lang.Enum} by calling searching matching enum + * names (ignoring case). + * + * @author Phillip Webb + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +class StringToEnumConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + Class enumType = targetType; + while (enumType != null && !enumType.isEnum()) { + enumType = enumType.getSuperclass(); + } + Assert.notNull(enumType, + "The target type " + targetType.getName() + " does not refer to an enum"); + return new StringToEnum(enumType); + } + + private class StringToEnum implements Converter { + + private final Class enumType; + + StringToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(String source) { + if (source.isEmpty()) { + return null; + } + source = source.trim(); + try { + return (T) Enum.valueOf(this.enumType, source); + } + catch (Exception ex) { + return findEnum(source); + } + } + + private T findEnum(String source) { + String name = getLettersAndDigits(source); + for (T candidate : (Set) EnumSet.allOf(this.enumType)) { + if (getLettersAndDigits(candidate.name()).equals(name)) { + return candidate; + } + } + throw new IllegalArgumentException("No enum constant " + + this.enumType.getCanonicalName() + "." + source); + } + + private String getLettersAndDigits(String name) { + StringBuilder canonicalName = new StringBuilder(name.length()); + name.chars().map((c) -> (char) c).filter(Character::isLetterOrDigit) + .map(Character::toLowerCase).forEach(canonicalName::append); + return canonicalName.toString(); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java new file mode 100644 index 0000000000..9ccb0c58c7 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.beans.PropertyEditor; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.springframework.core.convert.converter.Converter; + +/** + * {@link PropertyEditor} for {@link InetAddress} objects. + * + * @author Dave Syer + * @author Phillip Webb + */ +class StringToInetAddressConverter implements Converter { + + @Override + public InetAddress convert(String source) { + try { + return InetAddress.getByName(source); + } + catch (UnknownHostException ex) { + throw new IllegalStateException("Unknown host " + source, ex); + } + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java new file mode 100644 index 0000000000..f2256e0184 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Conversion support for configuration properties binding. + */ +package org.springframework.boot.context.properties.bind.convert; diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandler.java new file mode 100644 index 0000000000..c0935c1624 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.handler; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; + +/** + * {@link BindHandler} that can be used to ignore binding errors. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class IgnoreErrorsBindHandler extends AbstractBindHandler { + + public IgnoreErrorsBindHandler() { + super(); + } + + public IgnoreErrorsBindHandler(BindHandler parent) { + super(parent); + } + + @Override + public Object onFailure(ConfigurationPropertyName name, Bindable target, + BindContext context, Exception error) throws Exception { + return (target.getValue() == null ? null : target.getValue().get()); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandler.java new file mode 100644 index 0000000000..e5a26be049 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.handler; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; + +/** + * {@link BindHandler} to limit binding to only first level properties. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class IgnoreNestedPropertiesBindHandler extends AbstractBindHandler { + + public IgnoreNestedPropertiesBindHandler() { + super(); + } + + public IgnoreNestedPropertiesBindHandler(BindHandler parent) { + super(parent); + } + + @Override + public boolean onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + if (context.getDepth() > 1) { + return false; + } + return super.onStart(name, target, context); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java new file mode 100644 index 0000000000..fbb4a3fa9b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.handler; + +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.UnboundConfigurationPropertiesException; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; + +/** + * {@link BindHandler} to enforce that all configuration properties under the root name + * have been bound. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class NoUnboundElementsBindHandler extends AbstractBindHandler { + + private final Set boundNames = new HashSet<>(); + + public NoUnboundElementsBindHandler() { + super(); + } + + public NoUnboundElementsBindHandler(BindHandler parent) { + super(parent); + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + this.boundNames.add(name); + return super.onSuccess(name, target, context, result); + } + + @Override + public void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + if (context.getDepth() == 0) { + checkNoUnboundElements(name, context); + } + } + + private void checkNoUnboundElements(ConfigurationPropertyName name, + BindContext context) { + Set unbound = new TreeSet<>(); + for (ConfigurationPropertySource source : context.getSources()) { + ConfigurationPropertySource filtered = source + .filter((candidate) -> isUnbound(name, candidate)); + for (ConfigurationPropertyName unboundName : filtered) { + try { + unbound.add(filtered.getConfigurationProperty(unboundName)); + } + catch (Exception ex) { + } + } + } + if (!unbound.isEmpty()) { + throw new UnboundConfigurationPropertiesException(unbound); + } + } + + private boolean isUnbound(ConfigurationPropertyName name, + ConfigurationPropertyName candidate) { + return name.isAncestorOf(candidate) && !this.boundNames.contains(candidate); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/package-info.java new file mode 100644 index 0000000000..cc6bfc606f --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * General {@link org.springframework.boot.context.properties.bind.BindHandler + * BindHandler} implementations. + */ +package org.springframework.boot.context.properties.bind.handler; diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/package-info.java new file mode 100644 index 0000000000..8df7615696 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for {@code @ConfigurationProperties} binding. + */ +package org.springframework.boot.context.properties.bind; diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java new file mode 100644 index 0000000000..8ff51dc400 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.validation; + +import org.springframework.util.Assert; + +/** + * Error thrown when validation fails during a bind operation. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see ValidationErrors + * @see ValidationBindHandler + */ +public class BindValidationException extends RuntimeException { + + private final ValidationErrors validationErrors; + + BindValidationException(ValidationErrors validationErrors) { + Assert.notNull(validationErrors, "ValidationErrors must not be null"); + this.validationErrors = validationErrors; + } + + /** + * Return the validation errors that caused the exception. + * @return the validationErrors the validation errors + */ + public ValidationErrors getValidationErrors() { + return this.validationErrors; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldError.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldError.java new file mode 100644 index 0000000000..4a4643226c --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldError.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.validation; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; +import org.springframework.validation.FieldError; + +/** + * {@link FieldError} implementation that tracks the source {@link Origin}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +final class OriginTrackedFieldError extends FieldError implements OriginProvider { + + private final Origin origin; + + private OriginTrackedFieldError(FieldError fieldError, Origin origin) { + super(fieldError.getObjectName(), fieldError.getField(), + fieldError.getRejectedValue(), fieldError.isBindingFailure(), + fieldError.getCodes(), fieldError.getArguments(), + fieldError.getDefaultMessage()); + this.origin = origin; + } + + @Override + public Origin getOrigin() { + return this.origin; + } + + @Override + public String toString() { + if (this.origin == null) { + return toString(); + } + return super.toString() + "; origin " + this.origin; + } + + public static FieldError of(FieldError fieldError, Origin origin) { + if (fieldError == null || origin == null) { + return fieldError; + } + return new OriginTrackedFieldError(fieldError, origin); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java new file mode 100644 index 0000000000..c1a64269d7 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.validation; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; + +/** + * {@link BindHandler} to apply {@link Validator Validators} to bound results. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class ValidationBindHandler extends AbstractBindHandler { + + private final Validator[] validators; + + private boolean validate; + + private Set boundProperties = new LinkedHashSet<>(); + + public ValidationBindHandler(Validator... validators) { + super(); + this.validators = validators; + } + + public ValidationBindHandler(BindHandler parent, Validator... validators) { + super(parent); + this.validators = validators; + } + + @Override + public boolean onStart(ConfigurationPropertyName name, Bindable target, + BindContext context) { + if (context.getDepth() == 0) { + this.validate = shouldValidate(target); + } + return super.onStart(name, target, context); + } + + private boolean shouldValidate(Bindable target) { + Validated annotation = AnnotationUtils + .findAnnotation(target.getBoxedType().resolve(), Validated.class); + return (annotation != null); + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) { + if (context.getConfigurationProperty() != null) { + this.boundProperties.add(context.getConfigurationProperty()); + } + return super.onSuccess(name, target, context, result); + } + + @Override + public void onFinish(ConfigurationPropertyName name, Bindable target, + BindContext context, Object result) throws Exception { + if (this.validate) { + validate(name, target, result); + } + super.onFinish(name, target, context, result); + } + + private void validate(ConfigurationPropertyName name, Bindable target, + Object result) { + Object validationTarget = getValidationTarget(target, result); + Class validationType = target.getBoxedType().resolve(); + validate(name, validationTarget, validationType); + } + + private Object getValidationTarget(Bindable target, Object result) { + if (result != null) { + return result; + } + if (target.getValue() != null) { + return target.getValue().get(); + } + return null; + } + + private void validate(ConfigurationPropertyName name, Object target, Class type) { + if (target != null) { + BindingResult errors = new BeanPropertyBindingResult(target, name.toString()); + Arrays.stream(this.validators).filter((v) -> v.supports(type)) + .forEach((v) -> v.validate(target, errors)); + if (errors.hasErrors()) { + throwBindValidationException(name, errors); + } + } + } + + private void throwBindValidationException(ConfigurationPropertyName name, + BindingResult errors) { + Set boundProperties = this.boundProperties.stream() + .filter((property) -> name.isAncestorOf(property.getName())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + ValidationErrors validationErrors = new ValidationErrors(name, boundProperties, + errors.getAllErrors()); + throw new BindValidationException(validationErrors); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java new file mode 100644 index 0000000000..7001fb05c8 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.validation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.assertj.core.util.Objects; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; +import org.springframework.boot.origin.Origin; +import org.springframework.util.Assert; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +/** + * A collection of {@link ObjectError ObjectErrors} caused by bind validation failures. + * Where possible, included {@link FieldError FieldErrors} will be OriginProvider. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class ValidationErrors implements Iterable { + + private final ConfigurationPropertyName name; + + private final Set boundProperties; + + private final List errors; + + ValidationErrors(ConfigurationPropertyName name, + Set boundProperties, List errors) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(boundProperties, "BoundProperties must not be null"); + Assert.notNull(errors, "Errors must not be null"); + this.name = name; + this.boundProperties = Collections.unmodifiableSet(boundProperties); + this.errors = convertErrors(name, boundProperties, errors); + } + + private List convertErrors(ConfigurationPropertyName name, + Set boundProperties, List errors) { + List converted = new ArrayList<>(errors.size()); + for (ObjectError error : errors) { + converted.add(convertError(name, boundProperties, error)); + } + return Collections.unmodifiableList(converted); + } + + private ObjectError convertError(ConfigurationPropertyName name, + Set boundProperties, ObjectError error) { + if (error instanceof FieldError) { + return convertFieldError(name, boundProperties, (FieldError) error); + } + return error; + } + + private FieldError convertFieldError(ConfigurationPropertyName name, + Set boundProperties, FieldError error) { + if (error instanceof ObjectProvider) { + return error; + } + return OriginTrackedFieldError.of(error, + findFieldErrorOrigin(name, boundProperties, error)); + } + + private Origin findFieldErrorOrigin(ConfigurationPropertyName name, + Set boundProperties, FieldError error) { + for (ConfigurationProperty boundProperty : boundProperties) { + if (isForError(name, boundProperty.getName(), error)) { + return Origin.from(boundProperty); + } + } + return null; + } + + private boolean isForError(ConfigurationPropertyName name, + ConfigurationPropertyName boundPropertyName, FieldError error) { + return Objects.areEqual(boundPropertyName.getParent(), name) && boundPropertyName + .getElement().getValue(Form.UNIFORM).equalsIgnoreCase(error.getField()); + } + + /** + * Return the name of the item that was being validated. + * @return the name of the item + */ + public ConfigurationPropertyName getName() { + return this.name; + } + + /** + * Return the properties that were bound before validation failed. + * @return the boundProperties + */ + public Set getBoundProperties() { + return this.boundProperties; + } + + public boolean hasErrors() { + return !this.errors.isEmpty(); + } + + /** + * Return the list of all validation errors. + * @return the errors + */ + public List getAllErrors() { + return this.errors; + } + + @Override + public Iterator iterator() { + return this.errors.iterator(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/package-info.java new file mode 100644 index 0000000000..fdc8cc8436 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Binding validation support. + */ +package org.springframework.boot.context.properties.bind.validation; diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java new file mode 100644 index 0000000000..3510c971a1 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java @@ -0,0 +1,274 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.InOrder; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.core.ResolvableType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link ArrayBinder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ArrayBinderTests { + + private static final Bindable> INTEGER_LIST = Bindable + .listOf(Integer.class); + + private static final Bindable INTEGER_ARRAY = Bindable.of(Integer[].class); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void bindToArrayShouldReturnArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "1"); + source.put("foo[1]", "2"); + source.put("foo[2]", "3"); + this.sources.add(source); + Integer[] result = this.binder.bind("foo", INTEGER_ARRAY).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToCollectionShouldTriggerOnSuccess() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo[0]", "1", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + this.binder.bind("foo", INTEGER_LIST, handler); + InOrder inOrder = inOrder(handler); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo[0]")), + eq(Bindable.of(Integer.class)), any(), eq(1)); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(INTEGER_LIST), any(), isA(List.class)); + } + + @Test + public void bindToArrayShouldReturnPrimativeArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "1"); + source.put("foo[1]", "2"); + source.put("foo[2]", "3"); + this.sources.add(source); + int[] result = this.binder.bind("foo", Bindable.of(int[].class)).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToArrayWhenNestedShouldReturnPopulatedArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0][0]", "1"); + source.put("foo[0][1]", "2"); + source.put("foo[1][0]", "3"); + source.put("foo[1][1]", "4"); + this.sources.add(source); + ResolvableType type = ResolvableType.forArrayComponent(INTEGER_ARRAY.getType()); + Bindable target = Bindable.of(type); + Integer[][] result = this.binder.bind("foo", target).get(); + assertThat(result).hasSize(2); + assertThat(result[0]).containsExactly(1, 2); + assertThat(result[1]).containsExactly(3, 4); + } + + @Test + public void bindToArrayWhenNestedListShouldReturnPopulatedArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0][0]", "1"); + source.put("foo[0][1]", "2"); + source.put("foo[1][0]", "3"); + source.put("foo[1][1]", "4"); + this.sources.add(source); + ResolvableType type = ResolvableType.forArrayComponent(INTEGER_LIST.getType()); + Bindable[]> target = Bindable.of(type); + List[] result = this.binder.bind("foo", target).get(); + assertThat(result).hasSize(2); + assertThat(result[0]).containsExactly(1, 2); + assertThat(result[1]).containsExactly(3, 4); + } + + @Test + public void bindToArrayWhenNotInOrderShouldReturnPopulatedArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[1]", "2"); + source.put("foo[0]", "1"); + source.put("foo[2]", "3"); + this.sources.add(source); + Integer[] result = this.binder.bind("foo", INTEGER_ARRAY).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToArrayWhenNonSequentialShouldThrowException() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "2"); + source.put("foo[1]", "1"); + source.put("foo[3]", "3"); + this.sources.add(source); + try { + this.binder.bind("foo", INTEGER_ARRAY); + fail("No exception thrown"); + } + catch (BindException ex) { + Set unbound = ((UnboundConfigurationPropertiesException) ex + .getCause()).getUnboundProperties(); + assertThat(unbound.size()).isEqualTo(1); + ConfigurationProperty property = unbound.iterator().next(); + assertThat(property.getName().toString()).isEqualTo("foo[3]"); + assertThat(property.getValue()).isEqualTo("3"); + } + } + + @Test + public void bindToArrayWhenNonIterableShouldReturnPopulatedArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[1]", "2"); + source.put("foo[0]", "1"); + source.put("foo[2]", "3"); + source.setNonIterable(true); + this.sources.add(source); + Integer[] result = this.binder.bind("foo", INTEGER_ARRAY).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToArrayWhenMultipleSourceShouldOnlyUseFirst() throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("bar", "baz"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo[0]", "1"); + source2.put("foo[1]", "2"); + this.sources.add(source2); + MockConfigurationPropertySource source3 = new MockConfigurationPropertySource(); + source3.put("foo[0]", "7"); + source3.put("foo[1]", "8"); + source3.put("foo[2]", "9"); + this.sources.add(source3); + Integer[] result = this.binder.bind("foo", INTEGER_ARRAY).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToArrayWhenHasExistingCollectionShouldReplaceAllContents() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo[0]", "1")); + Integer[] existing = new Integer[2]; + existing[0] = 1000; + existing[1] = 1001; + Integer[] result = this.binder + .bind("foo", INTEGER_ARRAY.withExistingValue(existing)).get(); + assertThat(result).containsExactly(1); + } + + @Test + public void bindToArrayWhenNoValueShouldReturnUnbound() throws Exception { + this.sources.add(new MockConfigurationPropertySource("faf.bar", "1")); + BindResult result = this.binder.bind("foo", INTEGER_ARRAY); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToArrayShouldTriggerOnSuccess() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo[0]", "1", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + Bindable target = INTEGER_ARRAY; + this.binder.bind("foo", target, handler); + InOrder inOrder = inOrder(handler); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo[0]")), + eq(Bindable.of(Integer.class)), any(), eq(1)); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(target), any(), isA(Integer[].class)); + } + + @Test + public void bindToArrayWhenCommaListShouldReturnPopulatedArray() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "1,2,3")); + int[] result = this.binder.bind("foo", Bindable.of(int[].class)).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToArrayWhenCommaListAndIndexedShouldOnlyUseFirst() throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("foo", "1,2"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo[0]", "2"); + source2.put("foo[1]", "3"); + int[] result = this.binder.bind("foo", Bindable.of(int[].class)).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToArrayWhenIndexedAndCommaListShouldOnlyUseFirst() throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("foo[0]", "1"); + source1.put("foo[1]", "2"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo", "2,3"); + int[] result = this.binder.bind("foo", Bindable.of(int[].class)).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToArrayShouldBindCharArray() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "word")); + char[] result = this.binder.bind("foo", Bindable.of(char[].class)).get(); + assertThat(result).containsExactly("word".toCharArray()); + } + + @Test + public void bindToArrayWhenEmptyStringShouldReturnEmptyArray() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo", ""); + this.sources.add(source); + String[] result = this.binder.bind("foo", Bindable.of(String[].class)).get(); + assertThat(result).isNotNull().isEmpty(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java new file mode 100644 index 0000000000..fa05fc4204 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BeanPropertyName}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class BeanPropertyNameTests { + + @Test + public void toDashedCaseShouldConvertValue() { + assertThat(BeanPropertyName.toDashedForm("Foo")).isEqualTo("foo"); + assertThat(BeanPropertyName.toDashedForm("foo")).isEqualTo("foo"); + assertThat(BeanPropertyName.toDashedForm("fooBar")).isEqualTo("foo-bar"); + assertThat(BeanPropertyName.toDashedForm("foo_bar")).isEqualTo("foo-bar"); + assertThat(BeanPropertyName.toDashedForm("_foo_bar")).isEqualTo("-foo-bar"); + assertThat(BeanPropertyName.toDashedForm("foo_Bar")).isEqualTo("foo-bar"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java new file mode 100644 index 0000000000..1c4a67a68f --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindResultTests.java @@ -0,0 +1,234 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link BindResult}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class BindResultTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Mock + private Consumer consumer; + + @Mock + private Function mapper; + + @Mock + private Supplier supplier; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void getWhenHasValueShouldReturnValue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.get()).isEqualTo("foo"); + } + + @Test + public void getWhenHasNoValueShouldThrowException() throws Exception { + BindResult result = BindResult.of(null); + this.thrown.expect(NoSuchElementException.class); + this.thrown.expectMessage("No value bound"); + result.get(); + } + + @Test + public void isBoundWhenHasValueShouldReturnTrue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.isBound()).isTrue(); + } + + @Test + public void isBoundWhenHasNoValueShouldFalse() throws Exception { + BindResult result = BindResult.of(null); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void ifBoundWhenConsumerIsNullShouldThrowException() throws Exception { + BindResult result = BindResult.of("foo"); + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Consumer must not be null"); + result.ifBound(null); + } + + @Test + public void ifBoundWhenHasValueShouldCallConsumer() throws Exception { + BindResult result = BindResult.of("foo"); + result.ifBound(this.consumer); + verify(this.consumer).accept("foo"); + } + + @Test + public void ifBoundWhenHasNoValueShouldNotCallConsumer() throws Exception { + BindResult result = BindResult.of(null); + result.ifBound(this.consumer); + verifyZeroInteractions(this.consumer); + } + + @Test + public void mapWhenMapperIsNullShouldThrowException() throws Exception { + BindResult result = BindResult.of("foo"); + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Mapper must not be null"); + result.map(null); + } + + @Test + public void mapWhenHasValueShouldCallMapper() throws Exception { + BindResult result = BindResult.of("foo"); + given(this.mapper.apply("foo")).willReturn("bar"); + assertThat(result.map(this.mapper).get()).isEqualTo("bar"); + } + + @Test + public void mapWhenHasNoValueShouldNotCallMapper() throws Exception { + BindResult result = BindResult.of(null); + result.map(this.mapper); + verifyZeroInteractions(this.mapper); + } + + @Test + public void orElseWhenHasValueShouldReturnValue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.orElse("bar")).isEqualTo("foo"); + } + + @Test + public void orElseWhenHasValueNoShouldReturnOther() throws Exception { + BindResult result = BindResult.of(null); + assertThat(result.orElse("bar")).isEqualTo("bar"); + } + + @Test + public void orElseGetWhenHasValueShouldReturnValue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.orElseGet(this.supplier)).isEqualTo("foo"); + verifyZeroInteractions(this.supplier); + } + + @Test + public void orElseGetWhenHasValueNoShouldReturnOther() throws Exception { + BindResult result = BindResult.of(null); + given(this.supplier.get()).willReturn("bar"); + assertThat(result.orElseGet(this.supplier)).isEqualTo("bar"); + } + + @Test + public void orElseCreateWhenTypeIsNullShouldThrowException() throws Exception { + BindResult result = BindResult.of("foo"); + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Type must not be null"); + result.orElseCreate(null); + } + + @Test + public void orElseCreateWhenHasValueShouldReturnValue() throws Exception { + BindResult result = BindResult.of(new ExampleBean("foo")); + assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("foo"); + } + + @Test + public void orElseCreateWhenHasValueNoShouldReturnCreatedValue() throws Exception { + BindResult result = BindResult.of(null); + assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("new"); + } + + @Test + public void orElseThrowWhenHasValueShouldReturnValue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.orElseThrow(IOException::new)).isEqualTo("foo"); + } + + @Test + public void orElseThrowWhenHasNoValueShouldThrowException() throws Exception { + BindResult result = BindResult.of(null); + this.thrown.expect(IOException.class); + result.orElseThrow(IOException::new); + } + + @Test + public void hashCodeAndEquals() throws Exception { + BindResult result1 = BindResult.of("foo"); + BindResult result2 = BindResult.of("foo"); + BindResult result3 = BindResult.of("bar"); + BindResult result4 = BindResult.of(null); + assertThat(result1.hashCode()).isEqualTo(result2.hashCode()); + assertThat(result1).isEqualTo(result1).isEqualTo(result2).isNotEqualTo(result3) + .isNotEqualTo(result4); + } + + @Test + public void ofWhenHasValueShouldReturnBoundResultOfValue() throws Exception { + BindResult result = BindResult.of("foo"); + assertThat(result.isBound()).isTrue(); + assertThat(result.get()).isEqualTo("foo"); + } + + @Test + public void ofWhenValueIsNullShouldReturnUnbound() throws Exception { + BindResult result = BindResult.of(null); + assertThat(result.isBound()).isFalse(); + assertThat(result).isSameAs(BindResult.of(null)); + } + + static class ExampleBean { + + private final String value; + + ExampleBean() { + this.value = "new"; + } + + ExampleBean(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java new file mode 100644 index 0000000000..6ef83cd5c1 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link Bindable}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class BindableTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void ofClassWhenTypeIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Type must not be null"); + Bindable.of((Class) null); + } + + @Test + public void ofTypeWhenTypeIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Type must not be null"); + Bindable.of((ResolvableType) null); + } + + @Test + public void ofClassShouldSetType() throws Exception { + assertThat(Bindable.of(String.class).getType()) + .isEqualTo(ResolvableType.forClass(String.class)); + } + + @Test + public void ofTypeShouldSetType() throws Exception { + ResolvableType type = ResolvableType.forClass(String.class); + assertThat(Bindable.of(type).getType()).isEqualTo(type); + } + + @Test + public void ofInstanceShouldSetTypeAndExistingValue() throws Exception { + String instance = "foo"; + ResolvableType type = ResolvableType.forClass(String.class); + assertThat(Bindable.ofInstance(instance).getType()).isEqualTo(type); + assertThat(Bindable.ofInstance(instance).getValue().get()).isEqualTo("foo"); + } + + @Test + public void ofClassWithExistingValueShouldSetTypeAndExistingValue() throws Exception { + assertThat(Bindable.of(String.class).withExistingValue("foo").getValue().get()) + .isEqualTo("foo"); + } + + @Test + public void ofTypeWithExistingValueShouldSetTypeAndExistingValue() throws Exception { + assertThat(Bindable.of(ResolvableType.forClass(String.class)) + .withExistingValue("foo").getValue().get()).isEqualTo("foo"); + } + + @Test + public void ofTypeWhenExistingValueIsNotInstanceOfTypeShouldThrowException() + throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage( + "ExistingValue must be an instance of " + String.class.getName()); + Bindable.of(ResolvableType.forClass(String.class)).withExistingValue(123); + } + + @Test + public void ofTypeWhenPrimitiveWithExistingValueWrapperShouldNotThrowException() + throws Exception { + Bindable bindable = Bindable + .of(ResolvableType.forClass(int.class)).withExistingValue(123); + assertThat(bindable.getType().resolve()).isEqualTo(int.class); + assertThat(bindable.getValue().get()).isEqualTo(123); + } + + @Test + public void getBoxedTypeWhenNotBoxedShouldReturnType() throws Exception { + Bindable bindable = Bindable.of(String.class); + assertThat(bindable.getBoxedType()) + .isEqualTo(ResolvableType.forClass(String.class)); + } + + @Test + public void getBoxedTypeWhenPrimativeShouldReturnBoxedType() throws Exception { + Bindable bindable = Bindable.of(int.class); + assertThat(bindable.getType()).isEqualTo(ResolvableType.forClass(int.class)); + assertThat(bindable.getBoxedType()) + .isEqualTo(ResolvableType.forClass(Integer.class)); + } + + @Test + public void getBoxedTypeWhenPrimativeArrayShouldReturnBoxedType() throws Exception { + Bindable bindable = Bindable.of(int[].class); + assertThat(bindable.getType().getComponentType()) + .isEqualTo(ResolvableType.forClass(int.class)); + assertThat(bindable.getBoxedType().isArray()).isTrue(); + assertThat(bindable.getBoxedType().getComponentType()) + .isEqualTo(ResolvableType.forClass(Integer.class)); + } + + @Test + public void getAnnotationsShouldReturnEmptyArray() throws Exception { + assertThat(Bindable.of(String.class).getAnnotations()).isEmpty(); + } + + @Test + public void withAnnotationsShouldSetAnnotations() throws Exception { + Annotation annotation = mock(Annotation.class); + assertThat(Bindable.of(String.class).withAnnotations(annotation).getAnnotations()) + .containsExactly(annotation); + } + + @Test + public void toStringShouldShowDetails() throws Exception { + Annotation annotation = AnnotationUtils + .synthesizeAnnotation(TestAnnotation.class); + Bindable bindable = Bindable.of(String.class).withExistingValue("foo") + .withAnnotations(annotation); + System.out.println(bindable.toString()); + assertThat(bindable.toString()).contains("type = java.lang.String, " + + "value = 'provided', annotations = array[" + + "@org.springframework.boot.context.properties.bind." + + "BindableTests$TestAnnotation()]"); + } + + @Test + public void equalsAndHashcode() throws Exception { + Annotation annotation = AnnotationUtils + .synthesizeAnnotation(TestAnnotation.class); + Bindable bindable1 = Bindable.of(String.class).withExistingValue("foo") + .withAnnotations(annotation); + Bindable bindable2 = Bindable.of(String.class).withExistingValue("foo") + .withAnnotations(annotation); + Bindable bindable3 = Bindable.of(String.class).withExistingValue("fof") + .withAnnotations(annotation); + assertThat(bindable1.hashCode()).isEqualTo(bindable2.hashCode()); + assertThat(bindable1).isEqualTo(bindable1).isEqualTo(bindable2); + assertThat(bindable1).isEqualTo(bindable3); + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation { + + } + + static class TestNewInstance { + + private String foo = "hello world"; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + } + + static class TestNewInstanceWithNoDefaultConstructor { + + TestNewInstanceWithNoDefaultConstructor(String foo) { + this.foo = foo; + } + + private String foo = "hello world"; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java new file mode 100644 index 0000000000..9706f6e067 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java @@ -0,0 +1,223 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Answers; +import org.mockito.InOrder; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.test.context.support.TestPropertySourceUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link Binder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class BinderTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void createWhenSourcesIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Sources must not be null"); + new Binder((Iterable) null); + } + + @Test + public void bindWhenNameIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be null"); + this.binder.bind((ConfigurationPropertyName) null, Bindable.of(String.class), + BindHandler.DEFAULT); + } + + @Test + public void bindWhenTargetIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Target must not be null"); + this.binder.bind(ConfigurationPropertyName.of("foo"), null, BindHandler.DEFAULT); + } + + @Test + public void bindToValueWhenPropertyIsMissingShouldReturnUnbound() throws Exception { + this.sources.add(new MockConfigurationPropertySource()); + BindResult result = this.binder.bind("foo", Bindable.of(String.class)); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToValueShouldReturnPropertyValue() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", 123)); + BindResult result = this.binder.bind("foo", Bindable.of(Integer.class)); + assertThat(result.get()).isEqualTo(123); + } + + @Test + public void bindToValueShouldReturnPropertyValueFromSecondSource() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", 123)); + this.sources.add(new MockConfigurationPropertySource("bar", 234)); + BindResult result = this.binder.bind("bar", Bindable.of(Integer.class)); + assertThat(result.get()).isEqualTo(234); + } + + @Test + public void bindToValueShouldReturnConvertedPropertyValue() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "123")); + BindResult result = this.binder.bind("foo", Bindable.of(Integer.class)); + assertThat(result.get()).isEqualTo(123); + } + + @Test + public void bindToValueWhenMultipleCandidatesShouldReturnFirst() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", 123)); + this.sources.add(new MockConfigurationPropertySource("foo", 234)); + BindResult result = this.binder.bind("foo", Bindable.of(Integer.class)); + assertThat(result.get()).isEqualTo(123); + } + + @Test + public void bindToValueWithPlaceholdersShouldResolve() throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, "bar=23"); + this.sources.add(new MockConfigurationPropertySource("foo", "1${bar}")); + this.binder = new Binder(this.sources, + new PropertySourcesPlaceholdersResolver(environment)); + BindResult result = this.binder.bind("foo", Bindable.of(Integer.class)); + assertThat(result.get()).isEqualTo(123); + } + + @Test + public void bindToValueShouldTriggerOnSuccess() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "1", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + Bindable target = Bindable.of(Integer.class); + this.binder.bind("foo", target, handler); + InOrder ordered = inOrder(handler); + ordered.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(target), any(), eq(1)); + } + + @Test + public void bindToJavaBeanShouldReturnPopulatedBean() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.value", "bar")); + JavaBean result = this.binder.bind("foo", Bindable.of(JavaBean.class)).get(); + assertThat(result.getValue()).isEqualTo("bar"); + } + + @Test + public void bindToJavaBeanWhenNonIterableShouldReturnPopulatedBean() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource( + "foo.value", "bar"); + source.setNonIterable(true); + this.sources.add(source); + JavaBean result = this.binder.bind("foo", Bindable.of(JavaBean.class)).get(); + assertThat(result.getValue()).isEqualTo("bar"); + } + + @Test + public void bindToJavaBeanShouldTriggerOnSuccess() throws Exception { + this.sources + .add(new MockConfigurationPropertySource("foo.value", "bar", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + Bindable target = Bindable.of(JavaBean.class); + this.binder.bind("foo", target, handler); + InOrder inOrder = inOrder(handler); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo.value")), + eq(Bindable.of(String.class)), any(), eq("bar")); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(target), any(), isA(JavaBean.class)); + } + + @Test + public void bindWhenHasMalformedDateShouldThrowException() throws Exception { + this.thrown.expectCause(instanceOf(ConversionFailedException.class)); + this.sources.add(new MockConfigurationPropertySource("foo", "2014-04-01")); + this.binder.bind("foo", Bindable.of(LocalDate.class)); + } + + @Test + public void bindWhenHasAnnotationsShouldChangeConvertedValue() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "2014-04-01")); + DateTimeFormat annotation = AnnotationUtils.synthesizeAnnotation( + Collections.singletonMap("iso", DateTimeFormat.ISO.DATE), + DateTimeFormat.class, null); + LocalDate result = this.binder + .bind("foo", Bindable.of(LocalDate.class).withAnnotations(annotation)) + .get(); + assertThat(result.toString()).isEqualTo("2014-04-01"); + } + + public static class JavaBean { + + private String value; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + } + + public enum ExampleEnum { + + FOO_BAR, BAR_BAZ, BAZ_BOO + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java new file mode 100644 index 0000000000..9fbef4922e --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java @@ -0,0 +1,264 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.test.context.support.TestPropertySourceUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link CollectionBinder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class CollectionBinderTests { + + private static final Bindable> INTEGER_LIST = Bindable + .listOf(Integer.class); + + private static final Bindable> STRING_LIST = Bindable + .listOf(String.class); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void bindToCollectionShouldReturnPopulatedCollection() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "1"); + source.put("foo[1]", "2"); + source.put("foo[2]", "3"); + this.sources.add(source); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToCollectionWhenNestedShouldReturnPopulatedCollection() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0][0]", "1"); + source.put("foo[0][1]", "2"); + source.put("foo[1][0]", "3"); + source.put("foo[1][1]", "4"); + this.sources.add(source); + Bindable>> target = Bindable.of( + ResolvableType.forClassWithGenerics(List.class, INTEGER_LIST.getType())); + List> result = this.binder.bind("foo", target).get(); + assertThat(result).hasSize(2); + assertThat(result.get(0)).containsExactly(1, 2); + assertThat(result.get(1)).containsExactly(3, 4); + } + + @Test + public void bindToCollectionWhenNotInOrderShouldReturnPopulatedCollection() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[1]", "2"); + source.put("foo[0]", "1"); + source.put("foo[2]", "3"); + this.sources.add(source); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToCollectionWhenNonSequentialShouldThrowException() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "2"); + source.put("foo[1]", "1"); + source.put("foo[3]", "3"); + this.sources.add(source); + try { + this.binder.bind("foo", INTEGER_LIST); + fail("No exception thrown"); + } + catch (BindException ex) { + ex.printStackTrace(); + Set unbound = ((UnboundConfigurationPropertiesException) ex + .getCause()).getUnboundProperties(); + assertThat(unbound).hasSize(1); + ConfigurationProperty property = unbound.iterator().next(); + assertThat(property.getName().toString()).isEqualTo("foo[3]"); + assertThat(property.getValue()).isEqualTo("3"); + } + } + + @Test + public void bindToCollectionWhenNonIterableShouldReturnPopulatedCollection() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[1]", "2"); + source.put("foo[0]", "1"); + source.put("foo[2]", "3"); + source.setNonIterable(true); + this.sources.add(source); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToCollectionWhenMultipleSourceShouldOnlyUseFirst() throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("bar", "baz"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo[0]", "1"); + source2.put("foo[1]", "2"); + this.sources.add(source2); + MockConfigurationPropertySource source3 = new MockConfigurationPropertySource(); + source3.put("foo[0]", "7"); + source3.put("foo[1]", "8"); + source3.put("foo[2]", "9"); + this.sources.add(source3); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToCollectionWhenHasExistingCollectionShouldReplaceAllContents() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo[0]", "1")); + List existing = new LinkedList<>(); + existing.add(1000); + existing.add(1001); + List result = this.binder + .bind("foo", INTEGER_LIST.withExistingValue(existing)).get(); + assertThat(result).isExactlyInstanceOf(LinkedList.class); + assertThat(result).isSameAs(existing); + assertThat(result).containsExactly(1); + } + + @Test + public void bindToCollectionWhenHasExistingCollectionButNoValueShouldReturnUnbound() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("faf[0]", "1")); + List existing = new LinkedList<>(); + existing.add(1000); + BindResult> result = this.binder.bind("foo", + INTEGER_LIST.withExistingValue(existing)); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToCollectionShouldRespectCollectionType() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo[0]", "1")); + ResolvableType type = ResolvableType.forClassWithGenerics(LinkedList.class, + Integer.class); + Object defaultList = this.binder.bind("foo", INTEGER_LIST).get(); + Object customList = this.binder.bind("foo", Bindable.of(type)).get(); + assertThat(customList).isExactlyInstanceOf(LinkedList.class) + .isNotInstanceOf(defaultList.getClass()); + } + + @Test + public void bindToCollectionWhenNoValueShouldReturnUnbound() throws Exception { + this.sources.add(new MockConfigurationPropertySource("faf.bar", "1")); + BindResult> result = this.binder.bind("foo", INTEGER_LIST); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToCollectionWhenCommaListShouldReturnPopulatedCollection() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo", "1,2,3")); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + public void bindToCollectionWhenCommaListWithPlaceholdersShouldReturnPopulatedCollection() + throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, + "bar=1,2,3"); + this.binder = new Binder(this.sources, + new PropertySourcesPlaceholdersResolver(environment)); + this.sources.add(new MockConfigurationPropertySource("foo", "${bar}")); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2, 3); + + } + + @Test + public void bindToCollectionWhenCommaListAndIndexedShouldOnlyUseFirst() + throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("foo", "1,2"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo[0]", "2"); + source2.put("foo[1]", "3"); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToCollectionWhenIndexedAndCommaListShouldOnlyUseFirst() + throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("foo[0]", "1"); + source1.put("foo[1]", "2"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo", "2,3"); + List result = this.binder.bind("foo", INTEGER_LIST).get(); + assertThat(result).containsExactly(1, 2); + } + + @Test + public void bindToCollectionWhenItemContainsCommasShouldReturnPopulatedCollection() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "1,2"); + source.put("foo[1]", "3"); + this.sources.add(source); + List result = this.binder.bind("foo", STRING_LIST).get(); + assertThat(result).containsExactly("1,2", "3"); + } + + @Test + public void bindToCollectionWhenEmptyStringShouldReturnEmptyCollection() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo", ""); + this.sources.add(source); + List result = this.binder.bind("foo", STRING_LIST).get(); + assertThat(result).isNotNull().isEmpty(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java new file mode 100644 index 0000000000..3bc3f73ed4 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java @@ -0,0 +1,810 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.format.annotation.DateTimeFormat; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link JavaBeanBinder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class JavaBeanBinderTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void bindToClassShouldCreateBoundBean() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "12"); + source.put("foo.long-value", "34"); + source.put("foo.string-value", "foo"); + source.put("foo.enum-value", "foo-bar"); + this.sources.add(source); + ExampleValueBean bean = this.binder + .bind("foo", Bindable.of(ExampleValueBean.class)).get(); + assertThat(bean.getIntValue()).isEqualTo(12); + assertThat(bean.getLongValue()).isEqualTo(34); + assertThat(bean.getStringValue()).isEqualTo("foo"); + assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); + } + + @Test + public void bindToClassWhenHasNoPrefixShouldCreateBoundBean() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("int-value", "12"); + source.put("long-value", "34"); + source.put("string-value", "foo"); + source.put("enum-value", "foo-bar"); + this.sources.add(source); + ExampleValueBean bean = this.binder.bind(ConfigurationPropertyName.of(""), + Bindable.of(ExampleValueBean.class)).get(); + assertThat(bean.getIntValue()).isEqualTo(12); + assertThat(bean.getLongValue()).isEqualTo(34); + assertThat(bean.getStringValue()).isEqualTo("foo"); + assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); + } + + @Test + public void bindToInstanceShouldBindToInstance() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "12"); + source.put("foo.long-value", "34"); + source.put("foo.string-value", "foo"); + source.put("foo.enum-value", "foo-bar"); + this.sources.add(source); + ExampleValueBean bean = new ExampleValueBean(); + ExampleValueBean boundBean = this.binder + .bind("foo", Bindable.of(ExampleValueBean.class).withExistingValue(bean)) + .get(); + assertThat(boundBean).isSameAs(bean); + assertThat(bean.getIntValue()).isEqualTo(12); + assertThat(bean.getLongValue()).isEqualTo(34); + assertThat(bean.getStringValue()).isEqualTo("foo"); + assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); + } + + @Test + public void bindToInstanceWithNoPropertiesShouldReturnUnbound() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + this.sources.add(source); + ExampleDefaultsBean bean = new ExampleDefaultsBean(); + BindResult boundBean = this.binder.bind("foo", + Bindable.of(ExampleDefaultsBean.class).withExistingValue(bean)); + assertThat(boundBean.isBound()).isFalse(); + assertThat(bean.getFoo()).isEqualTo(123); + assertThat(bean.getBar()).isEqualTo(456); + } + + @Test + public void bindToClassShouldLeaveDefaults() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "999"); + this.sources.add(source); + ExampleDefaultsBean bean = this.binder + .bind("foo", Bindable.of(ExampleDefaultsBean.class)).get(); + assertThat(bean.getFoo()).isEqualTo(123); + assertThat(bean.getBar()).isEqualTo(999); + } + + @Test + public void bindToExistingInstanceShouldLeaveDefaults() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "999"); + this.sources.add(source); + ExampleDefaultsBean bean = new ExampleDefaultsBean(); + bean.setFoo(888); + ExampleDefaultsBean boundBean = this.binder + .bind("foo", + Bindable.of(ExampleDefaultsBean.class).withExistingValue(bean)) + .get(); + assertThat(boundBean).isSameAs(bean); + assertThat(bean.getFoo()).isEqualTo(888); + assertThat(bean.getBar()).isEqualTo(999); + } + + @Test + public void bindToClassShouldBindToMap() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.map.foo-bar", "1"); + source.put("foo.map.bar-baz", "2"); + this.sources.add(source); + ExampleMapBean bean = this.binder.bind("foo", Bindable.of(ExampleMapBean.class)) + .get(); + assertThat(bean.getMap()).containsExactly(entry(ExampleEnum.FOO_BAR, 1), + entry(ExampleEnum.BAR_BAZ, 2)); + } + + @Test + public void bindToClassShouldBindToList() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.list[0]", "foo-bar"); + source.put("foo.list[1]", "bar-baz"); + this.sources.add(source); + ExampleListBean bean = this.binder.bind("foo", Bindable.of(ExampleListBean.class)) + .get(); + assertThat(bean.getList()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToListIfUnboundElementsPresentShouldThrowException() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.list[0]", "foo-bar"); + source.put("foo.list[2]", "bar-baz"); + this.sources.add(source); + this.thrown.expect(BindException.class); + this.thrown.expectCause( + Matchers.instanceOf(UnboundConfigurationPropertiesException.class)); + this.binder.bind("foo", Bindable.of(ExampleListBean.class)); + } + + @Test + public void bindToClassShouldBindToSet() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.set[0]", "foo-bar"); + source.put("foo.set[1]", "bar-baz"); + this.sources.add(source); + ExampleSetBean bean = this.binder.bind("foo", Bindable.of(ExampleSetBean.class)) + .get(); + assertThat(bean.getSet()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToClassShouldBindToCollection() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.collection[0]", "foo-bar"); + source.put("foo.collection[1]", "bar-baz"); + this.sources.add(source); + ExampleCollectionBean bean = this.binder + .bind("foo", Bindable.of(ExampleCollectionBean.class)).get(); + assertThat(bean.getCollection()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToClassWhenHasNoSetterShouldBindToMap() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.map.foo-bar", "1"); + source.put("foo.map.bar-baz", "2"); + this.sources.add(source); + ExampleMapBeanWithoutSetter bean = this.binder + .bind("foo", Bindable.of(ExampleMapBeanWithoutSetter.class)).get(); + assertThat(bean.getMap()).containsExactly(entry(ExampleEnum.FOO_BAR, 1), + entry(ExampleEnum.BAR_BAZ, 2)); + } + + @Test + public void bindToClassWhenHasNoSetterShouldBindToList() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.list[0]", "foo-bar"); + source.put("foo.list[1]", "bar-baz"); + this.sources.add(source); + ExampleListBeanWithoutSetter bean = this.binder + .bind("foo", Bindable.of(ExampleListBeanWithoutSetter.class)).get(); + assertThat(bean.getList()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToClassWhenHasNoSetterShouldBindToSet() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.set[0]", "foo-bar"); + source.put("foo.set[1]", "bar-baz"); + this.sources.add(source); + ExampleSetBeanWithoutSetter bean = this.binder + .bind("foo", Bindable.of(ExampleSetBeanWithoutSetter.class)).get(); + assertThat(bean.getSet()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToClassWhenHasNoSetterShouldBindToCollection() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.collection[0]", "foo-bar"); + source.put("foo.collection[1]", "bar-baz"); + this.sources.add(source); + ExampleCollectionBeanWithoutSetter bean = this.binder + .bind("foo", Bindable.of(ExampleCollectionBeanWithoutSetter.class)).get(); + assertThat(bean.getCollection()).containsExactly(ExampleEnum.FOO_BAR, + ExampleEnum.BAR_BAZ); + } + + @Test + public void bindToClassShouldBindNested() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean.int-value", "123"); + source.put("foo.value-bean.string-value", "foo"); + this.sources.add(source); + ExampleNestedBean bean = this.binder + .bind("foo", Bindable.of(ExampleNestedBean.class)).get(); + assertThat(bean.getValueBean().getIntValue()).isEqualTo(123); + assertThat(bean.getValueBean().getStringValue()).isEqualTo("foo"); + } + + @Test + public void bindToClassWhenIterableShouldBindNestedBasedOnInstance() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean.int-value", "123"); + source.put("foo.value-bean.string-value", "foo"); + this.sources.add(source); + ExampleNestedBeanWithoutSetterOrType bean = this.binder + .bind("foo", Bindable.of(ExampleNestedBeanWithoutSetterOrType.class)) + .get(); + ExampleValueBean valueBean = (ExampleValueBean) bean.getValueBean(); + assertThat(valueBean.getIntValue()).isEqualTo(123); + assertThat(valueBean.getStringValue()).isEqualTo("foo"); + } + + @Test + public void bindToClassWhenNotIterableShouldNotBindNestedBasedOnInstance() + throws Exception { + // If we can't tell that binding will happen, we don't want to randomly invoke + // getters on the class and cause side effects + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean.int-value", "123"); + source.put("foo.value-bean.string-value", "foo"); + source.setNonIterable(true); + this.sources.add(source); + BindResult bean = this.binder.bind("foo", + Bindable.of(ExampleNestedBeanWithoutSetterOrType.class)); + assertThat(bean.isBound()).isFalse(); + } + + @Test + public void bindToClassWhenHasNoSetterShouldBindNested() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean.int-value", "123"); + source.put("foo.value-bean.string-value", "foo"); + this.sources.add(source); + ExampleNestedBeanWithoutSetter bean = this.binder + .bind("foo", Bindable.of(ExampleNestedBeanWithoutSetter.class)).get(); + assertThat(bean.getValueBean().getIntValue()).isEqualTo(123); + assertThat(bean.getValueBean().getStringValue()).isEqualTo("foo"); + } + + @Test + public void bindToClassWhenHasNoSetterAndImmutableShouldThrowException() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.nested.foo", "bar"); + this.sources.add(source); + this.thrown.expect(BindException.class); + this.binder.bind("foo", + Bindable.of(ExampleImmutableNestedBeanWithoutSetter.class)); + } + + @Test + public void bindToInstanceWhenNoNestedShouldLeaveNestedAsNull() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("faf.value-bean.int-value", "123"); + this.sources.add(source); + ExampleNestedBean bean = new ExampleNestedBean(); + BindResult boundBean = this.binder.bind("foo", + Bindable.of(ExampleNestedBean.class).withExistingValue(bean)); + assertThat(boundBean.isBound()).isFalse(); + assertThat(bean.getValueBean()).isNull(); + } + + @Test + public void bindToClassWhenPropertiesMissingShouldReturnUnbound() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("faf.int-value", "12"); + this.sources.add(source); + BindResult bean = this.binder.bind("foo", + Bindable.of(ExampleValueBean.class)); + assertThat(bean.isBound()).isFalse(); + } + + @Test + public void bindToClassWhenNoDefaultConstructorShouldReturnUnbound() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value", "bar"); + this.sources.add(source); + BindResult bean = this.binder.bind("foo", + Bindable.of(ExampleWithNonDefaultConstructor.class)); + assertThat(bean.isBound()).isFalse(); + } + + @Test + public void bindToInstanceWhenNoDefaultConstructorShouldBind() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value", "bar"); + this.sources.add(source); + ExampleWithNonDefaultConstructor bean = new ExampleWithNonDefaultConstructor( + "faf"); + ExampleWithNonDefaultConstructor boundBean = this.binder.bind("foo", Bindable + .of(ExampleWithNonDefaultConstructor.class).withExistingValue(bean)) + .get(); + assertThat(boundBean).isSameAs(bean); + assertThat(bean.getValue()).isEqualTo("bar"); + } + + @Test + public void bindToClassShouldBindHierarchy() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "123"); + source.put("foo.long-value", "456"); + this.sources.add(source); + ExampleSubclassBean bean = this.binder + .bind("foo", Bindable.of(ExampleSubclassBean.class)).get(); + assertThat(bean.getIntValue()).isEqualTo(123); + assertThat(bean.getLongValue()).isEqualTo(456); + } + + @Test + public void bindToClassWhenPropertyCannotBeConvertedShouldThrowException() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.int-value", "foo")); + this.thrown.expect(BindException.class); + this.binder.bind("foo", Bindable.of(ExampleValueBean.class)); + } + + @Test + public void bindToClassWhenPropertyCannotBeConvertedAndIgnoreErrorsShouldNotSetValue() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "12"); + source.put("foo.long-value", "bang"); + source.put("foo.string-value", "foo"); + source.put("foo.enum-value", "foo-bar"); + this.sources.add(source); + IgnoreErrorsBindHandler handler = new IgnoreErrorsBindHandler(); + ExampleValueBean bean = this.binder + .bind("foo", Bindable.of(ExampleValueBean.class), handler).get(); + assertThat(bean.getIntValue()).isEqualTo(12); + assertThat(bean.getLongValue()).isEqualTo(0); + assertThat(bean.getStringValue()).isEqualTo("foo"); + assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); + } + + @Test + public void bindToClassWhenMismatchedGetSetShouldBind() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value", "123"); + this.sources.add(source); + ExampleMismatchBean bean = this.binder + .bind("foo", Bindable.of(ExampleMismatchBean.class)).get(); + assertThat(bean.getValue()).isEqualTo("123"); + } + + @Test + public void bindToClassShouldNotInvokeExtraMethods() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource( + "foo.value", "123"); + source.setNonIterable(true); + this.sources.add(source); + ExampleWithThrowingGetters bean = this.binder + .bind("foo", Bindable.of(ExampleWithThrowingGetters.class)).get(); + assertThat(bean.getValue()).isEqualTo(123); + } + + @Test + public void bindToClassWithSelfReferenceShouldBind() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value", "123"); + this.sources.add(source); + ExampleWithSelfReference bean = this.binder + .bind("foo", Bindable.of(ExampleWithSelfReference.class)).get(); + assertThat(bean.getValue()).isEqualTo(123); + } + + @Test + public void bindtoInstanceWithExistingValueShouldReturnUnbound() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + this.sources.add(source); + ExampleNestedBean existingValue = new ExampleNestedBean(); + ExampleValueBean valueBean = new ExampleValueBean(); + existingValue.setValueBean(valueBean); + BindResult result = this.binder.bind("foo", + Bindable.of(ExampleNestedBean.class).withExistingValue(existingValue)); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindWithAnnotations() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.date", "2014-04-01"); + this.sources.add(source); + ConverterAnnotatedExampleBean bean = this.binder + .bind("foo", Bindable.of(ConverterAnnotatedExampleBean.class)).get(); + assertThat(bean.getDate().toString()).isEqualTo("2014-04-01"); + } + + public static class ExampleValueBean { + + private int intValue; + + private long longValue; + + private String stringValue; + + private ExampleEnum enumValue; + + public int getIntValue() { + return this.intValue; + } + + public void setIntValue(int intValue) { + this.intValue = intValue; + } + + public long getLongValue() { + return this.longValue; + } + + public void setLongValue(long longValue) { + this.longValue = longValue; + } + + public String getStringValue() { + return this.stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + public ExampleEnum getEnumValue() { + return this.enumValue; + } + + public void setEnumValue(ExampleEnum enumValue) { + this.enumValue = enumValue; + } + + } + + public static class ExampleDefaultsBean { + + private int foo = 123; + + private int bar = 456; + + public int getFoo() { + return this.foo; + } + + public void setFoo(int foo) { + this.foo = foo; + } + + public int getBar() { + return this.bar; + } + + public void setBar(int bar) { + this.bar = bar; + } + + } + + public static class ExampleMapBean { + + private Map map; + + public Map getMap() { + return this.map; + } + + public void setMap(Map map) { + this.map = map; + } + + } + + public static class ExampleListBean { + + private List list; + + public List getList() { + return this.list; + } + + public void setList(List list) { + this.list = list; + } + + } + + public static class ExampleSetBean { + + private Set set; + + public Set getSet() { + return this.set; + } + + public void setSet(Set set) { + this.set = set; + } + + } + + public static class ExampleCollectionBean { + + private Collection collection; + + public Collection getCollection() { + return this.collection; + } + + public void setCollection(Collection collection) { + this.collection = collection; + } + + } + + public static class ExampleMapBeanWithoutSetter { + + private Map map = new LinkedHashMap<>(); + + public Map getMap() { + return this.map; + } + + } + + public static class ExampleListBeanWithoutSetter { + + private List list = new ArrayList<>(); + + public List getList() { + return this.list; + } + + } + + public static class ExampleSetBeanWithoutSetter { + + private Set set = new LinkedHashSet<>(); + + public Set getSet() { + return this.set; + } + + } + + public static class ExampleCollectionBeanWithoutSetter { + + private Collection collection = new ArrayList<>(); + + public Collection getCollection() { + return this.collection; + } + + } + + public static class ExampleNestedBean { + + private ExampleValueBean valueBean; + + public ExampleValueBean getValueBean() { + return this.valueBean; + } + + public void setValueBean(ExampleValueBean valueBean) { + this.valueBean = valueBean; + } + + } + + public static class ExampleNestedBeanWithoutSetter { + + private ExampleValueBean valueBean = new ExampleValueBean(); + + public ExampleValueBean getValueBean() { + return this.valueBean; + } + + } + + public static class ExampleNestedBeanWithoutSetterOrType { + + private ExampleValueBean valueBean = new ExampleValueBean(); + + public Object getValueBean() { + return this.valueBean; + } + + } + + public static class ExampleImmutableNestedBeanWithoutSetter { + + private NestedImmutable nested = new NestedImmutable(); + + public NestedImmutable getNested() { + return this.nested; + } + + public static class NestedImmutable { + + public String getFoo() { + return "foo"; + } + + } + + } + + public static class ExampleWithNonDefaultConstructor { + + private String value; + + public ExampleWithNonDefaultConstructor(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + } + + public abstract static class ExampleSuperClassBean { + + private int intValue; + + public int getIntValue() { + return this.intValue; + } + + public void setIntValue(int intValue) { + this.intValue = intValue; + } + + } + + public static class ExampleSubclassBean extends ExampleSuperClassBean { + + private long longValue; + + public long getLongValue() { + return this.longValue; + } + + public void setLongValue(long longValue) { + this.longValue = longValue; + } + + } + + public static class ExampleMismatchBean { + + private int value; + + public String getValue() { + return String.valueOf(this.value); + } + + public void setValue(int value) { + this.value = value; + } + + } + + public static class ExampleWithThrowingGetters { + + private int value; + + public int getValue() { + return this.value; + } + + public void setValue(int value) { + this.value = value; + } + + public List getNames() { + throw new RuntimeException(); + } + + public ExampleValueBean getNested() { + throw new RuntimeException(); + } + + } + + public static class ExampleWithSelfReference { + + private int value; + + private ExampleWithSelfReference self; + + public int getValue() { + return this.value; + } + + public void setValue(int value) { + this.value = value; + } + + public ExampleWithSelfReference getSelf() { + return this.self; + } + + public void setSelf(ExampleWithSelfReference self) { + this.self = self; + } + + } + + public enum ExampleEnum { + + FOO_BAR, + + BAR_BAZ + + } + + public static class ConverterAnnotatedExampleBean { + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + private LocalDate date; + + public LocalDate getDate() { + return this.date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java new file mode 100644 index 0000000000..6776686110 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -0,0 +1,355 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.InOrder; + +import org.springframework.boot.context.properties.bind.BinderTests.ExampleEnum; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.test.context.support.TestPropertySourceUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link MapBinder}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class MapBinderTests { + + private static final Bindable> STRING_STRING_MAP = Bindable + .mapOf(String.class, String.class); + + private static final Bindable> STRING_INTEGER_MAP = Bindable + .mapOf(String.class, Integer.class); + + private static final Bindable> INTEGER_INTEGER_MAP = Bindable + .mapOf(Integer.class, Integer.class); + + private static final Bindable> STRING_OBJECT_MAP = Bindable + .mapOf(String.class, Object.class); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void bindToMapShouldReturnPopulatedMap() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "1"); + source.put("foo.[baz]", "2"); + source.put("foo[BiNg]", "3"); + this.sources.add(source); + Map result = this.binder.bind("foo", STRING_STRING_MAP).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry("bar", "1"); + assertThat(result).containsEntry("baz", "2"); + assertThat(result).containsEntry("BiNg", "3"); + } + + @Test + @SuppressWarnings("unchecked") + public void bindToMapWithEmptyPrefix() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "1"); + this.sources.add(source); + Map result = this.binder.bind("", STRING_OBJECT_MAP).get(); + assertThat((Map) result.get("foo")).containsEntry("bar", "1"); + } + + @Test + public void bindToMapShouldConvertMapValue() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "1"); + source.put("foo.[baz]", "2"); + source.put("foo[BiNg]", "3"); + source.put("faf.bar", "x"); + this.sources.add(source); + Map result = this.binder.bind("foo", STRING_INTEGER_MAP).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry("bar", 1); + assertThat(result).containsEntry("baz", 2); + assertThat(result).containsEntry("BiNg", 3); + } + + @Test + public void bindToMapShouldBindToMapValue() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(Map.class, + ResolvableType.forClass(String.class), STRING_INTEGER_MAP.getType()); + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar.baz", "1"); + source.put("foo.bar.bin", "2"); + source.put("foo.far.baz", "3"); + source.put("foo.far.bin", "4"); + source.put("faf.far.bin", "x"); + this.sources.add(source); + Map> result = this.binder + .bind("foo", Bindable.>>of(type)).get(); + System.out.println(result); + assertThat(result).hasSize(2); + assertThat(result.get("bar")).containsEntry("baz", 1).containsEntry("bin", 2); + assertThat(result.get("far")).containsEntry("baz", 3).containsEntry("bin", 4); + } + + @Test + public void bindToMapShouldBindNestedMapValue() throws Exception { + ResolvableType nestedType = ResolvableType.forClassWithGenerics(Map.class, + ResolvableType.forClass(String.class), STRING_INTEGER_MAP.getType()); + ResolvableType type = ResolvableType.forClassWithGenerics(Map.class, + ResolvableType.forClass(String.class), nestedType); + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.nested.bar.baz", "1"); + source.put("foo.nested.bar.bin", "2"); + source.put("foo.nested.far.baz", "3"); + source.put("foo.nested.far.bin", "4"); + source.put("faf.nested.far.bin", "x"); + this.sources.add(source); + Bindable>>> target = Bindable + .of(type); + Map>> result = this.binder + .bind("foo", target).get(); + Map> nested = result.get("nested"); + assertThat(nested).hasSize(2); + assertThat(nested.get("bar")).containsEntry("baz", 1).containsEntry("bin", 2); + assertThat(nested.get("far")).containsEntry("baz", 3).containsEntry("bin", 4); + } + + @Test + @SuppressWarnings("unchecked") + public void bindToMapWhenMapValueIsObjectShouldBindNestedMapValue() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.nested.bar.baz", "1"); + source.put("foo.nested.bar.bin", "2"); + source.put("foo.nested.far.baz", "3"); + source.put("foo.nested.far.bin", "4"); + source.put("faf.nested.far.bin", "x"); + this.sources.add(source); + Map result = this.binder + .bind("foo", Bindable.mapOf(String.class, Object.class)).get(); + Map nested = (Map) result.get("nested"); + assertThat(nested).hasSize(2); + Map bar = (Map) nested.get("bar"); + assertThat(bar).containsEntry("baz", "1").containsEntry("bin", "2"); + Map far = (Map) nested.get("far"); + assertThat(far).containsEntry("baz", "3").containsEntry("bin", "4"); + } + + @Test + public void bindToMapWhenMapValueIsObjectAndNoRootShouldBindNestedMapValue() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("commit.id", "abcdefg"); + source.put("branch", "master"); + source.put("foo", "bar"); + this.sources.add(source); + Map result = this.binder + .bind("", Bindable.mapOf(String.class, Object.class)).get(); + assertThat(result.get("commit")) + .isEqualTo(Collections.singletonMap("id", "abcdefg")); + assertThat(result.get("branch")).isEqualTo("master"); + assertThat(result.get("foo")).isEqualTo("bar"); + } + + @Test + public void bindToMapWhenEmptyRootNameShouldBindMap() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("bar.baz", "1"); + source.put("bar.bin", "2"); + this.sources.add(source); + Map result = this.binder.bind("", STRING_INTEGER_MAP).get(); + assertThat(result).hasSize(2); + assertThat(result).containsEntry("bar.baz", 1).containsEntry("bar.bin", 2); + } + + @Test + public void bindToMapWhenMultipleCandidateShouldBindFirst() throws Exception { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("foo.bar", "1"); + source1.put("foo.baz", "2"); + this.sources.add(source1); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("foo.baz", "3"); + source2.put("foo.bin", "4"); + this.sources.add(source2); + Map result = this.binder.bind("foo", STRING_INTEGER_MAP).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry("bar", 1); + assertThat(result).containsEntry("baz", 2); + assertThat(result).containsEntry("bin", 4); + } + + @Test + public void bindToMapWhenMultipleInSameSourceCandidateShouldBindFirst() + throws Exception { + Map map = new HashMap<>(); + map.put("foo.bar", "1"); + map.put("foo.b-az", "2"); + map.put("foo.ba-z", "3"); + map.put("foo.bin", "4"); + MapConfigurationPropertySource propertySource = new MapConfigurationPropertySource( + map); + this.sources.add(propertySource); + Map result = this.binder.bind("foo", STRING_INTEGER_MAP).get(); + assertThat(result).hasSize(4); + assertThat(result).containsEntry("bar", 1); + assertThat(result).containsEntry("b-az", 2); + assertThat(result).containsEntry("ba-z", 3); + assertThat(result).containsEntry("bin", 4); + } + + @Test + public void bindToMapWhenHasExistingMapShouldReplaceOnlyNewContents() + throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.bar", "1")); + Map existing = new HashMap<>(); + existing.put("bar", 1000); + existing.put("baz", 1001); + Bindable> target = STRING_INTEGER_MAP + .withExistingValue(existing); + Map result = this.binder.bind("foo", target).get(); + assertThat(result).isExactlyInstanceOf(HashMap.class); + assertThat(result).isSameAs(existing); + assertThat(result).hasSize(2); + assertThat(result).containsEntry("bar", 1); + assertThat(result).containsEntry("baz", 1001); + } + + @Test + public void bindToMapShouldRespectMapType() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.bar", "1")); + ResolvableType type = ResolvableType.forClassWithGenerics(HashMap.class, + String.class, Integer.class); + Object defaultMap = this.binder.bind("foo", STRING_INTEGER_MAP).get(); + Object customMap = this.binder.bind("foo", Bindable.of(type)).get(); + assertThat(customMap).isExactlyInstanceOf(HashMap.class) + .isNotInstanceOf(defaultMap.getClass()); + } + + @Test + public void bindToMapWhenNoValueShouldReturnUnbound() throws Exception { + this.sources.add(new MockConfigurationPropertySource("faf.bar", "1")); + BindResult> result = this.binder.bind("foo", + STRING_INTEGER_MAP); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToMapShouldConvertKey() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0]", "1"); + source.put("foo[1]", "2"); + source.put("foo[9]", "3"); + this.sources.add(source); + Map result = this.binder.bind("foo", INTEGER_INTEGER_MAP).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry(0, 1); + assertThat(result).containsEntry(1, 2); + assertThat(result).containsEntry(9, 3); + } + + @Test + public void bindToMapShouldBeGreedyForStrings() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.aaa.bbb.ccc", "b"); + source.put("foo.bbb.ccc.ddd", "a"); + source.put("foo.ccc.ddd.eee", "r"); + this.sources.add(source); + Map result = this.binder.bind("foo", STRING_STRING_MAP).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry("aaa.bbb.ccc", "b"); + assertThat(result).containsEntry("bbb.ccc.ddd", "a"); + assertThat(result).containsEntry("ccc.ddd.eee", "r"); + } + + @Test + public void bindToMapShouldBeGreedyForScalars() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.aaa.bbb.ccc", "foo-bar"); + source.put("foo.bbb.ccc.ddd", "BAR_BAZ"); + source.put("foo.ccc.ddd.eee", "bazboo"); + this.sources.add(source); + Map result = this.binder + .bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)).get(); + assertThat(result).hasSize(3); + assertThat(result).containsEntry("aaa.bbb.ccc", ExampleEnum.FOO_BAR); + assertThat(result).containsEntry("bbb.ccc.ddd", ExampleEnum.BAR_BAZ); + assertThat(result).containsEntry("ccc.ddd.eee", ExampleEnum.BAZ_BOO); + } + + @Test + public void bindToMapWithPlaceholdersShouldBeGreedyForScalars() throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, "foo=boo"); + MockConfigurationPropertySource source = new MockConfigurationPropertySource( + "foo.aaa.bbb.ccc", "baz-${foo}"); + this.sources.add(source); + this.binder = new Binder(this.sources, + new PropertySourcesPlaceholdersResolver(environment)); + Map result = this.binder + .bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)).get(); + assertThat(result).containsEntry("aaa.bbb.ccc", ExampleEnum.BAZ_BOO); + } + + @Test + public void bindToMapWithNoPropertiesShouldReturnUnbound() throws Exception { + this.binder = new Binder(this.sources); + BindResult> result = this.binder.bind("foo", + Bindable.mapOf(String.class, ExampleEnum.class)); + assertThat(result.isBound()).isFalse(); + } + + @Test + public void bindToMapShouldTriggerOnSuccess() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.bar", "1", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + Bindable> target = STRING_INTEGER_MAP; + this.binder.bind("foo", target, handler); + InOrder inOrder = inOrder(handler); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo.bar")), + eq(Bindable.of(Integer.class)), any(), eq(1)); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(target), any(), isA(Map.class)); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolverTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolverTests.java new file mode 100644 index 0000000000..2bcbf40e5e --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/PropertySourcesPlaceholdersResolverTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySources; +import org.springframework.util.PropertyPlaceholderHelper; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertySourcesPlaceholdersResolver}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class PropertySourcesPlaceholdersResolverTests { + + private PropertySourcesPlaceholdersResolver resolver; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void placeholderResolverIfEnvironmentNullShouldThrowException() + throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Environment must not be null"); + new PropertySourcesPlaceholdersResolver((Environment) null); + } + + @Test + public void resolveIfPlaceholderPresentResolvesProperty() { + MutablePropertySources sources = getPropertySources(); + this.resolver = new PropertySourcesPlaceholdersResolver(sources); + Object resolved = this.resolver.resolvePlaceholders("${FOO}"); + assertThat(resolved).isEqualTo("hello world"); + } + + @Test + public void resolveIfPlaceholderAbsentUsesDefault() { + this.resolver = new PropertySourcesPlaceholdersResolver((PropertySources) null); + Object resolved = this.resolver.resolvePlaceholders("${FOO:bar}"); + assertThat(resolved).isEqualTo("bar"); + } + + @Test + public void resolveIfPlaceholderAbsentAndNoDefaultShouldThrowException() { + this.resolver = new PropertySourcesPlaceholdersResolver((PropertySources) null); + this.thrown.expect(IllegalArgumentException.class); + this.thrown + .expectMessage("Could not resolve placeholder 'FOO' in value \"${FOO}\""); + this.resolver.resolvePlaceholders("${FOO}"); + } + + @Test + public void resolveIfHelperPresentShouldUseIt() { + MutablePropertySources sources = getPropertySources(); + TestPropertyPlaceholderHelper helper = new TestPropertyPlaceholderHelper("$<", + ">"); + this.resolver = new PropertySourcesPlaceholdersResolver(sources, helper); + Object resolved = this.resolver.resolvePlaceholders("$"); + assertThat(resolved).isEqualTo("hello world"); + } + + private MutablePropertySources getPropertySources() { + MutablePropertySources sources = new MutablePropertySources(); + Map source = new HashMap<>(); + source.put("FOO", "hello world"); + sources.addFirst(new MapPropertySource("test", source)); + return sources; + } + + static class TestPropertyPlaceholderHelper extends PropertyPlaceholderHelper { + + TestPropertyPlaceholderHelper(String placeholderPrefix, + String placeholderSuffix) { + super(placeholderPrefix, placeholderSuffix); + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java new file mode 100644 index 0000000000..517834f823 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.junit.AssumptionViolatedException; + +/** + * Base class for {@link InetAddress} tests. + * + * @author Phillip Webb + */ +public abstract class AbstractInetAddressTests { + + public void assumeResolves(String host) { + try { + InetAddress.getByName(host); + } + catch (UnknownHostException ex) { + throw new AssumptionViolatedException("Host " + host + " not resolvable", ex); + } + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java new file mode 100644 index 0000000000..6674c6879e --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.io.InputStream; +import java.net.InetAddress; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link BinderConversionService}. + * + * @author Phillip Webb + */ +public class BinderConversionServiceTests { + + private ConversionService delegate; + + private BinderConversionService service; + + @Before + public void setup() { + this.delegate = mock(ConversionService.class); + this.service = new BinderConversionService(this.delegate); + } + + @Test + public void createConversionServiceShouldAcceptNullConversionService() + throws Exception { + BinderConversionService service = new BinderConversionService(null); + assertThat(service.canConvert(String.class, TestEnum.class)).isTrue(); + assertThat(service.canConvert(TypeDescriptor.valueOf(String.class), + TypeDescriptor.valueOf(TestEnum.class))).isTrue(); + assertThat(service.convert("ONE", TestEnum.class)).isEqualTo(TestEnum.ONE); + assertThat(service.convert("ONE", TypeDescriptor.valueOf(String.class), + TypeDescriptor.valueOf(TestEnum.class))).isEqualTo(TestEnum.ONE); + } + + @Test + public void canConvertShouldDelegateToConversionService() throws Exception { + Class from = String.class; + Class to = InputStream.class; + given(this.delegate.canConvert(from, to)).willReturn(true); + assertThat(this.service.canConvert(from, to)).isEqualTo(true); + verify(this.delegate).canConvert(from, to); + } + + @Test + public void canConvertTypeDescriptorShouldDelegateToConversionService() + throws Exception { + TypeDescriptor from = TypeDescriptor.valueOf(String.class); + TypeDescriptor to = TypeDescriptor.valueOf(InputStream.class); + given(this.delegate.canConvert(from, to)).willReturn(true); + assertThat(this.service.canConvert(from, to)).isEqualTo(true); + verify(this.delegate).canConvert(from, to); + } + + @Test + public void convertShouldDelegateToConversionService() throws Exception { + String from = "foo"; + InputStream to = mock(InputStream.class); + given(this.delegate.convert(from, InputStream.class)).willReturn(to); + assertThat(this.service.convert(from, InputStream.class)).isEqualTo(to); + verify(this.delegate).convert(from, InputStream.class); + } + + @Test + public void convertTargetTypeShouldDelegateToConversionService() throws Exception { + String from = "foo"; + InputStream to = mock(InputStream.class); + TypeDescriptor fromType = TypeDescriptor.valueOf(String.class); + TypeDescriptor toType = TypeDescriptor.valueOf(InputStream.class); + given(this.delegate.convert(from, fromType, toType)).willReturn(to); + assertThat(this.service.convert(from, fromType, toType)).isEqualTo(to); + verify(this.delegate).convert(from, fromType, toType); + } + + @Test + public void convertShouldSwallowDelegateConversionFailedException() throws Exception { + given(this.delegate.convert("one", TestEnum.class)) + .willThrow(new ConversionFailedException(null, null, null, null)); + assertThat(this.service.convert("one", TestEnum.class)).isEqualTo(TestEnum.ONE); + verify(this.delegate).convert("one", TestEnum.class); + } + + @Test + public void conversionServiceShouldSupportEnums() throws Exception { + this.service = new BinderConversionService(null); + assertThat(this.service.canConvert(String.class, TestEnum.class)).isTrue(); + assertThat(this.service.convert("one", TestEnum.class)).isEqualTo(TestEnum.ONE); + assertThat(this.service.convert("t-w-o", TestEnum.class)).isEqualTo(TestEnum.TWO); + } + + @Test + public void conversionServiceShouldSupportStringToCharArray() throws Exception { + this.service = new BinderConversionService(null); + assertThat(this.service.canConvert(String.class, char[].class)).isTrue(); + assertThat(this.service.convert("test", char[].class)).containsExactly('t', 'e', + 's', 't'); + } + + @Test + public void conversionServiceShouldSupportStringToInetAddress() throws Exception { + this.service = new BinderConversionService(null); + assertThat(this.service.canConvert(String.class, InetAddress.class)).isTrue(); + } + + @Test + public void conversionServiceShouldSupportInetAddressToString() throws Exception { + this.service = new BinderConversionService(null); + assertThat(this.service.canConvert(InetAddress.class, String.class)).isTrue(); + } + + @Test + public void conversionServiceShouldSupportStringToResource() throws Exception { + this.service = new BinderConversionService(null); + Resource resource = this.service.convert( + "org/springframework/boot/context/properties/bind/convert/resource.txt", + Resource.class); + assertThat(resource).isNotNull(); + } + + @Test + public void conversionServiceShouldSupportStringToClass() throws Exception { + this.service = new BinderConversionService(null); + Class converted = this.service.convert(InputStream.class.getName(), + Class.class); + assertThat(converted).isEqualTo(InputStream.class); + } + + enum TestEnum { + + ONE, TWO + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java new file mode 100644 index 0000000000..4e3402254a --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.net.InetAddress; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link InetAddressToStringConverter}. + * + * @author Phillip Webb + */ +public class InetAddressToStringConverterTests extends AbstractInetAddressTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private InetAddressToStringConverter converter = new InetAddressToStringConverter(); + + @Test + public void convertShouldConvertToHostAddress() throws Exception { + assumeResolves("example.com"); + InetAddress address = InetAddress.getByName("example.com"); + String converted = this.converter.convert(address); + assertThat(converted).isEqualTo(address.getHostAddress()); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java new file mode 100644 index 0000000000..0e3b6fcf8e --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; + +import org.junit.Test; + +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertyEditorConverter}. + * + * @author Phillip Webb + */ +public class PropertyEditorConverterTests { + + private final PropertyEditorConverter converter = new PropertyEditorConverter(); + + @Test + public void matchesShouldLimitToPropertyEditor() { + String converted = new SimpleTypeConverter().convertIfNecessary(123, + String.class); + assertThat(converted).isEqualTo("123"); + // Even though the SimpleTypeConverter can convert, we should limit to just + // PropertyEditors not implicit support + assertThat(this.converter.matches(TypeDescriptor.valueOf(Integer.class), + TypeDescriptor.valueOf(String.class))).isFalse(); + } + + @Test + public void convertShouldSupportConventionBasedEditors() throws Exception { + String source = "org/springframework/boot/context/properties/bind/convert/resource.txt"; + TypeDescriptor sourceType = TypeDescriptor.forObject(source); + TypeDescriptor targetType = TypeDescriptor.valueOf(Resource.class); + assertThat(this.converter.matches(sourceType, targetType)).isTrue(); + Object converted = this.converter.convert(source, sourceType, targetType); + assertThat(converted).isNotNull().isInstanceOf(Resource.class); + assertThat(converted.toString()).endsWith("resource.txt]"); + } + + @Test + public void convertShouldSupportDefaultEditors() throws Exception { + String source = "en_UK"; + TypeDescriptor sourceType = TypeDescriptor.forObject(source); + TypeDescriptor targetType = TypeDescriptor.valueOf(Locale.class); + assertThat(this.converter.matches(sourceType, targetType)).isTrue(); + Object converted = this.converter.convert(source, sourceType, targetType); + assertThat(converted).isNotNull().isInstanceOf(Locale.class); + assertThat(converted.toString()).endsWith("en_UK"); + } + + @Test + public void matchShouldNotMatchCollection() throws Exception { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + assertThat(this.converter.matches(sourceType, + TypeDescriptor.valueOf(Collection.class))).isFalse(); + assertThat(this.converter.matches(sourceType, TypeDescriptor.valueOf(List.class))) + .isFalse(); + assertThat(this.converter.matches(sourceType, TypeDescriptor.valueOf(Set.class))) + .isFalse(); + } + + @Test + public void matchShouldNotMatchMap() throws Exception { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + assertThat(this.converter.matches(sourceType, TypeDescriptor.valueOf(Map.class))) + .isFalse(); + assertThat(this.converter.matches(sourceType, + TypeDescriptor.valueOf(SortedMap.class))).isFalse(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptorTests.java new file mode 100644 index 0000000000..12e0a8616e --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/ResolvableTypeDescriptorTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.lang.annotation.Annotation; +import java.util.List; + +import org.junit.Test; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.TypeDescriptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ResolvableTypeDescriptor}. + * + * @author Phillip Webb + */ +public class ResolvableTypeDescriptorTests { + + @Test + public void forBindableShouldIncludeType() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(List.class, + String.class); + Bindable bindable = Bindable.of(type); + TypeDescriptor descriptor = ResolvableTypeDescriptor.forBindable(bindable); + assertThat(descriptor.getResolvableType()).isEqualTo(type); + } + + @Test + public void forBindableShouldIncludeAnnotations() throws Exception { + Annotation annotation = AnnotationUtils.synthesizeAnnotation(Test.class); + Bindable bindable = Bindable.of(String.class).withAnnotations(annotation); + TypeDescriptor descriptor = ResolvableTypeDescriptor.forBindable(bindable); + assertThat(descriptor.getAnnotations()).containsExactly(annotation); + } + + @Test + public void forTypeShouldIncludeType() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(List.class, + String.class); + TypeDescriptor descriptor = ResolvableTypeDescriptor.forType(type); + assertThat(descriptor.getResolvableType()).isEqualTo(type); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java new file mode 100644 index 0000000000..a880d56e0d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StringToCharArrayConverter}. + * + * @author Phillip Webb + */ +public class StringToCharArrayConverterTests { + + private StringToCharArrayConverter converter = new StringToCharArrayConverter(); + + @Test + public void convertShouldConvertSource() throws Exception { + char[] converted = this.converter.convert("test"); + assertThat(converted).containsExactly('t', 'e', 's', 't'); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java new file mode 100644 index 0000000000..f7274b1062 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import org.junit.Test; + +import org.springframework.core.convert.converter.Converter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StringToEnumConverterFactory}. + * + * @author Phillip Webb + */ +public class StringToEnumConverterFactoryTests { + + private StringToEnumConverterFactory factory = new StringToEnumConverterFactory(); + + @Test + public void getConverterShouldReturnConverter() { + Converter converter = this.factory.getConverter(TestEnum.class); + assertThat(converter).isNotNull(); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void getConverterWhenEnumSubclassShouldReturnConverter() throws Exception { + Converter converter = this.factory + .getConverter((Class) TestSubclassEnum.ONE.getClass()); + assertThat(converter).isNotNull(); + } + + @Test + public void convertWhenExactMatchShouldConvertValue() throws Exception { + Converter converter = this.factory.getConverter(TestEnum.class); + assertThat(converter.convert("")).isNull(); + assertThat(converter.convert("ONE")).isEqualTo(TestEnum.ONE); + assertThat(converter.convert("TWO")).isEqualTo(TestEnum.TWO); + assertThat(converter.convert("THREE_AND_FOUR")) + .isEqualTo(TestEnum.THREE_AND_FOUR); + } + + @Test + public void convertWhenFuzzyMatchShouldConvertValue() throws Exception { + Converter converter = this.factory.getConverter(TestEnum.class); + assertThat(converter.convert("")).isNull(); + assertThat(converter.convert("one")).isEqualTo(TestEnum.ONE); + assertThat(converter.convert("tWo")).isEqualTo(TestEnum.TWO); + assertThat(converter.convert("three_and_four")) + .isEqualTo(TestEnum.THREE_AND_FOUR); + assertThat(converter.convert("threeandfour")).isEqualTo(TestEnum.THREE_AND_FOUR); + assertThat(converter.convert("three-and-four")) + .isEqualTo(TestEnum.THREE_AND_FOUR); + assertThat(converter.convert("threeAndFour")).isEqualTo(TestEnum.THREE_AND_FOUR); + } + + enum TestEnum { + + ONE, TWO, THREE_AND_FOUR + + } + + enum TestSubclassEnum { + + ONE { + + @Override + public String toString() { + return "foo"; + } + + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java new file mode 100644 index 0000000000..ca15cfed5d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.convert; + +import java.net.InetAddress; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StringToInetAddressConverter}. + * + * @author Phillip Webb + */ +public class StringToInetAddressConverterTests extends AbstractInetAddressTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private StringToInetAddressConverter converter = new StringToInetAddressConverter(); + + @Test + public void convertWhenHostDoesNotExistShouldThrowException() { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Unknown host"); + this.converter.convert("ireallydontexist.example.com"); + } + + @Test + public void convertWhenHostExistsShouldConvert() throws Exception { + assumeResolves("example.com"); + InetAddress converted = this.converter.convert("example.com"); + assertThat(converted.toString()).startsWith("example.com"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandlerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandlerTests.java new file mode 100644 index 0000000000..efc1870f40 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreErrorsBindHandlerTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.handler; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IgnoreErrorsBindHandler}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class IgnoreErrorsBindHandlerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + } + + @Test + public void bindWhenNotIgnoringErrorsShouldFail() throws Exception { + this.thrown.expect(BindException.class); + this.binder.bind("example", Bindable.of(Example.class)); + } + + @Test + public void bindWhenIgnoringErrorsShouldBind() throws Exception { + Example bound = this.binder.bind("example", Bindable.of(Example.class), + new IgnoreErrorsBindHandler()).get(); + assertThat(bound.getFoo()).isEqualTo(0); + } + + public static class Example { + + private int foo; + + public int getFoo() { + return this.foo; + } + + public void setFoo(int foo) { + this.foo = foo; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandlerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandlerTests.java new file mode 100644 index 0000000000..f58d5c76fa --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/IgnoreNestedPropertiesBindHandlerTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.handler; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IgnoreNestedPropertiesBindHandler}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class IgnoreNestedPropertiesBindHandlerTests { + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "foovalue"); + source.put("example.nested.bar", "barvalue"); + this.sources.add(source); + this.binder = new Binder(this.sources); + } + + @Test + public void bindWhenNotIngoringNestedShouldBindAll() throws Exception { + Example bound = this.binder.bind("example", Bindable.of(Example.class)).get(); + assertThat(bound.getFoo()).isEqualTo("foovalue"); + assertThat(bound.getNested().getBar()).isEqualTo("barvalue"); + } + + @Test + public void bindWhenIngoringNestedShouldFilterNested() throws Exception { + Example bound = this.binder.bind("example", Bindable.of(Example.class), + new IgnoreNestedPropertiesBindHandler()).get(); + assertThat(bound.getFoo()).isEqualTo("foovalue"); + assertThat(bound.getNested()).isNull(); + } + + public static class Example { + + private String foo; + + private ExampleNested nested; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public ExampleNested getNested() { + return this.nested; + } + + public void setNested(ExampleNested nested) { + this.nested = nested; + } + + } + + public static class ExampleNested { + + private String bar; + + public String getBar() { + return this.bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java new file mode 100644 index 0000000000..0733005839 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.handler; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link NoUnboundElementsBindHandler}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class NoUnboundElementsBindHandlerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Test + public void bindWhenNotUsingNoUnboundElementsHandlerShouldBind() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "bar"); + source.put("example.baz", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + Example bound = this.binder + .bind(ConfigurationPropertyName.of("example"), Bindable.of(Example.class)) + .get(); + assertThat(bound.getFoo()).isEqualTo("bar"); + } + + @Test + public void bindWhenUsingNoUnboundElementsHandlerShouldBind() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + Example bound = this.binder.bind("example", Bindable.of(Example.class), + new NoUnboundElementsBindHandler()).get(); + assertThat(bound.getFoo()).isEqualTo("bar"); + } + + @Test + public void bindWhenUsingNoUnboundElementsHandlerThrowException() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "bar"); + source.put("example.baz", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + try { + this.binder.bind("example", Bindable.of(Example.class), + new NoUnboundElementsBindHandler()); + fail("did not throw"); + } + catch (BindException ex) { + assertThat(ex.getCause().getMessage()) + .contains("The elements [example.baz] were left unbound"); + } + } + + @Test + public void bindWhenUsingNoUnboundElementsHandlerShouldBindIfPrefixDifferent() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo", "bar"); + source.put("other.baz", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + Example bound = this.binder.bind("example", Bindable.of(Example.class), + new NoUnboundElementsBindHandler()).get(); + assertThat(bound.getFoo()).isEqualTo("bar"); + } + + public static class Example { + + private String foo; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/test/PackagePrivateBeanBindingTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/test/PackagePrivateBeanBindingTests.java new file mode 100644 index 0000000000..70a4c830e0 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/test/PackagePrivateBeanBindingTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.test; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Binder} using package private Java beans. + * + * @author Madhura Bhave + */ +public class PackagePrivateBeanBindingTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private Binder binder; + + private ConfigurationPropertyName name; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + this.name = ConfigurationPropertyName.of("foo"); + } + + @Test + public void bindToPackagePrivateClassShouldBindToInstance() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "999"); + this.sources.add(source); + ExamplePackagePrivateBean bean = this.binder + .bind(this.name, Bindable.of(ExamplePackagePrivateBean.class)).get(); + assertThat(bean.getBar()).isEqualTo(999); + } + + static class ExamplePackagePrivateBean { + + private int bar; + + public int getBar() { + return this.bar; + } + + public void setBar(int bar) { + this.bar = bar; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/BindValidationExceptionTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/BindValidationExceptionTests.java new file mode 100644 index 0000000000..083cc67a09 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/BindValidationExceptionTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.validation; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BindValidationException}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class BindValidationExceptionTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenValidationErrorsIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ValidationErrors must not be null"); + new BindValidationException(null); + } + + @Test + public void getValidationErrorsShouldReturnValidationErrors() throws Exception { + ValidationErrors errors = mock(ValidationErrors.class); + BindValidationException exception = new BindValidationException(errors); + assertThat(exception.getValidationErrors()).isEqualTo(errors); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldErrorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldErrorTests.java new file mode 100644 index 0000000000..53980fa2e6 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/OriginTrackedFieldErrorTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.validation; + +import org.junit.Test; + +import org.springframework.boot.origin.MockOrigin; +import org.springframework.boot.origin.Origin; +import org.springframework.validation.FieldError; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OriginTrackedFieldError}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class OriginTrackedFieldErrorTests { + + private static final FieldError FIELD_ERROR = new FieldError("foo", "bar", "faf"); + + private static final Origin ORIGIN = MockOrigin.of("afile"); + + @Test + public void ofWhenFieldErrorIsNullShouldReturnNull() throws Exception { + assertThat(OriginTrackedFieldError.of(null, ORIGIN)).isNull(); + } + + @Test + public void ofWhenOriginIsNullShouldReturnFieldErrorWithoutOrigin() throws Exception { + assertThat(OriginTrackedFieldError.of(FIELD_ERROR, null)).isSameAs(FIELD_ERROR); + } + + @Test + public void ofShouldReturnOriginCapableFieldError() throws Exception { + FieldError fieldError = OriginTrackedFieldError.of(FIELD_ERROR, ORIGIN); + assertThat(fieldError.getObjectName()).isEqualTo("foo"); + assertThat(fieldError.getField()).isEqualTo("bar"); + assertThat(Origin.from(fieldError)).isEqualTo(ORIGIN); + } + + @Test + public void toStringShouldAddOrigin() throws Exception { + assertThat(OriginTrackedFieldError.of(FIELD_ERROR, ORIGIN).toString()).isEqualTo( + "Field error in object 'foo' on field 'bar': rejected value [null]" + + "; codes []; arguments []; default message [faf]; origin afile"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java new file mode 100644 index 0000000000..deb414dfdf --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java @@ -0,0 +1,249 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.validation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.boot.origin.Origin; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.validation.annotation.Validated; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.instanceOf; + +/** + * Tests for {@link ValidationBindHandler}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ValidationBindHandlerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private List sources = new ArrayList<>(); + + private ValidationBindHandler handler; + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + this.handler = new ValidationBindHandler(validator); + } + + @Test + public void bindShouldBindWithoutHandler() { + this.sources.add(new MockConfigurationPropertySource("foo.age", 4)); + ExampleValidatedBean bean = this.binder + .bind("foo", Bindable.of(ExampleValidatedBean.class)).get(); + assertThat(bean.getAge()).isEqualTo(4); + } + + @Test + public void bindShouldFailWithHandler() { + this.sources.add(new MockConfigurationPropertySource("foo.age", 4)); + this.thrown.expect(BindException.class); + this.thrown.expectCause(instanceOf(BindValidationException.class)); + this.binder.bind("foo", Bindable.of(ExampleValidatedBean.class), this.handler); + } + + @Test + public void bindShouldValidateNestedProperties() throws Exception { + this.sources.add(new MockConfigurationPropertySource("foo.nested.age", 4)); + this.thrown.expect(BindException.class); + this.thrown.expectCause(instanceOf(BindValidationException.class)); + this.binder.bind("foo", Bindable.of(ExampleValidatedWithNestedBean.class), + this.handler); + } + + @Test + public void bindShouldFailWithAccessToOrigin() { + this.sources.add(new MockConfigurationPropertySource("foo.age", 4, "file")); + BindValidationException cause = bindAndExpectValidationError( + () -> this.binder.bind(ConfigurationPropertyName.of("foo"), + Bindable.of(ExampleValidatedBean.class), this.handler)); + ObjectError objectError = cause.getValidationErrors().getAllErrors().get(0); + assertThat(Origin.from(objectError).toString()).isEqualTo("file"); + } + + @Test + public void bindShouldFailWithAccessToBoundProperties() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.nested.name", "baz"); + source.put("foo.nested.age", "4"); + source.put("faf.bar", "baz"); + this.sources.add(source); + BindValidationException cause = bindAndExpectValidationError( + () -> this.binder.bind(ConfigurationPropertyName.of("foo"), + Bindable.of(ExampleValidatedWithNestedBean.class), this.handler)); + Set boundProperties = cause.getValidationErrors() + .getBoundProperties(); + assertThat(boundProperties).extracting((p) -> p.getName().toString()) + .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; + } + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationErrorsTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationErrorsTests.java new file mode 100644 index 0000000000..b1c829aeb9 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationErrorsTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind.validation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.origin.MockOrigin; +import org.springframework.boot.origin.Origin; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ValidationErrors}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ValidationErrorsTests { + + private static final ConfigurationPropertyName NAME = ConfigurationPropertyName + .of("foo"); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenNameIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be null"); + new ValidationErrors(null, Collections.emptySet(), Collections.emptyList()); + } + + @Test + public void createWhenBoundPropertiesIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("BoundProperties must not be null"); + new ValidationErrors(NAME, null, Collections.emptyList()); + } + + @Test + public void createWhenErrorsIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Errors must not be null"); + new ValidationErrors(NAME, Collections.emptySet(), null); + } + + @Test + public void getNameShouldReturnName() throws Exception { + ConfigurationPropertyName name = NAME; + ValidationErrors errors = new ValidationErrors(name, Collections.emptySet(), + Collections.emptyList()); + assertThat((Object) errors.getName()).isEqualTo(name); + } + + @Test + public void getBoundPropertiesShouldReturnBoundProperties() throws Exception { + Set boundProperties = new LinkedHashSet<>(); + boundProperties.add(new ConfigurationProperty(NAME, "foo", null)); + ValidationErrors errors = new ValidationErrors(NAME, boundProperties, + Collections.emptyList()); + assertThat(errors.getBoundProperties()).isEqualTo(boundProperties); + } + + @Test + public void getErrorsShouldReturnErrors() throws Exception { + List allErrors = new ArrayList<>(); + allErrors.add(new ObjectError("foo", "bar")); + ValidationErrors errors = new ValidationErrors(NAME, Collections.emptySet(), + allErrors); + assertThat(errors.getAllErrors()).isEqualTo(allErrors); + } + + @Test + public void iteratorShouldIterateErrors() throws Exception { + List allErrors = new ArrayList<>(); + allErrors.add(new ObjectError("foo", "bar")); + ValidationErrors errors = new ValidationErrors(NAME, Collections.emptySet(), + allErrors); + assertThat(errors.iterator()).containsExactlyElementsOf(allErrors); + } + + @Test + public void getErrorsShouldAdaptFieldErrorsToBeOriginProviders() throws Exception { + Set boundProperties = new LinkedHashSet<>(); + ConfigurationPropertyName name1 = ConfigurationPropertyName.of("foo.bar"); + Origin origin1 = MockOrigin.of("line1"); + boundProperties.add(new ConfigurationProperty(name1, "boot", origin1)); + ConfigurationPropertyName name2 = ConfigurationPropertyName.of("foo.baz.bar"); + Origin origin2 = MockOrigin.of("line2"); + boundProperties.add(new ConfigurationProperty(name2, "boot", origin2)); + List allErrors = new ArrayList<>(); + allErrors.add(new FieldError("objectname", "bar", "message")); + ValidationErrors errors = new ValidationErrors( + ConfigurationPropertyName.of("foo.baz"), boundProperties, allErrors); + assertThat(Origin.from(errors.getAllErrors().get(0))).isEqualTo(origin2); + } + +} diff --git a/spring-boot/src/test/resources/org/springframework/boot/context/properties/bind/convert/resource.txt b/spring-boot/src/test/resources/org/springframework/boot/context/properties/bind/convert/resource.txt new file mode 100644 index 0000000000..9daeafb986 --- /dev/null +++ b/spring-boot/src/test/resources/org/springframework/boot/context/properties/bind/convert/resource.txt @@ -0,0 +1 @@ +test From 16f9ef4f6af8233a624aaec85becf69b0d326aea Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 25 Apr 2017 10:59:17 -0700 Subject: [PATCH 05/19] Attach ConfigurationPropertiesSource Update `SpringApplication` to automatically attached the `ConfigurationPropertiesSource` when a `SpringApplication` runs. See gh-4910 --- .../org/springframework/boot/SpringApplication.java | 2 ++ .../springframework/boot/SpringApplicationTests.java | 11 ++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 8579230fe1..823c77310e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -42,6 +42,7 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.boot.Banner.Mode; import org.springframework.boot.bind.PropertiesConfigurationFactory; import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationListener; @@ -375,6 +376,7 @@ public class SpringApplication { && this.webApplicationType == WebApplicationType.NONE) { environment = convertToStandardEnvironment(environment); } + ConfigurationPropertySources.attach(environment); return environment; } diff --git a/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 5dad01ce3b..fc66965f9e 100644 --- a/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -47,6 +48,7 @@ import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEven import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationStartingEvent; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.testutil.InternalOutputCapture; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; @@ -866,9 +868,12 @@ public class SpringApplicationTests { assertThat(this.context.getEnvironment()) .isNotInstanceOf(StandardServletEnvironment.class); assertThat(this.context.getEnvironment().getProperty("foo")).isEqualTo("bar"); - assertThat(this.context.getEnvironment().getPropertySources().iterator().next() - .getName()).isEqualTo( - TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); + Iterator> iterator = this.context.getEnvironment() + .getPropertySources().iterator(); + assertThat(iterator.next().getName()) + .isEqualTo(ConfigurationPropertySources.PROPERTY_SOURCE_NAME); + assertThat(iterator.next().getName()).isEqualTo( + TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); } @Test From 834aa09300b4ed6f75f0841fc35836b6f4f9944e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 25 Apr 2017 11:30:27 -0700 Subject: [PATCH 06/19] Remove ConfigurationProperySource Remove `ConfigurationProperySource` from `PropertySourcesPropertyValues` so that the deprecated code to continues to work. See gh-4910 --- .../boot/bind/PropertySourcesPropertyValues.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java index 1fb4cc29ec..a7a7c4632d 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java @@ -25,8 +25,10 @@ import java.util.regex.Pattern; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValue; import org.springframework.beans.PropertyValues; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.core.env.PropertySourcesPropertyResolver; @@ -108,6 +110,10 @@ public class PropertySourcesPropertyValues implements PropertyValues { PropertyNamePatternsMatcher includes, boolean resolvePlaceholders) { Assert.notNull(propertySources, "PropertySources must not be null"); Assert.notNull(includes, "Includes must not be null"); + MutablePropertySources mutablePropertySources = new MutablePropertySources( + propertySources); + mutablePropertySources.remove(ConfigurationPropertySources.PROPERTY_SOURCE_NAME); + propertySources = mutablePropertySources; this.propertySources = propertySources; this.nonEnumerableFallbackNames = nonEnumerableFallbackNames; this.includes = includes; From d4ef523e6419b7dd8d0dcbb358f479aceed544c0 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 25 Apr 2017 13:23:08 -0700 Subject: [PATCH 07/19] Use new configuration properties in spring-boot Update `spring-boot` to use the new configuration properties support. See gh-9000 --- .../org/springframework/boot/ImageBanner.java | 16 +- .../boot/SpringApplication.java | 22 +-- .../FileEncodingApplicationListener.java | 44 ++--- .../config/AnsiOutputApplicationListener.java | 26 ++- .../config/ConfigFileApplicationListener.java | 120 ++++-------- .../logging/LoggingApplicationListener.java | 25 +-- ...urationPropertiesBindingPostProcessor.java | 176 ++++++++---------- .../boot/logging/LoggingSystemProperties.java | 34 +++- .../logback/DefaultLogbackConfiguration.java | 18 +- .../logging/logback/SpringPropertyAction.java | 17 +- .../boot/system/ApplicationPidFileWriter.java | 5 +- .../yaml/SpringProfileDocumentMatcher.java | 20 +- .../FileEncodingApplicationListenerTests.java | 4 +- .../ConfigFileApplicationListenerTests.java | 6 +- .../LoggingApplicationListenerTests.java | 5 +- ...onPropertiesBindingPostProcessorTests.java | 103 ++++++---- .../EnableConfigurationPropertiesTests.java | 55 +----- .../SpringBootJoranConfiguratorTests.java | 4 +- .../SpringProfileDocumentMatcherTests.java | 4 +- 19 files changed, 321 insertions(+), 383 deletions(-) diff --git a/spring-boot/src/main/java/org/springframework/boot/ImageBanner.java b/spring-boot/src/main/java/org/springframework/boot/ImageBanner.java index 75cdfc5ae6..4c8c7fbf09 100644 --- a/spring-boot/src/main/java/org/springframework/boot/ImageBanner.java +++ b/spring-boot/src/main/java/org/springframework/boot/ImageBanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -33,9 +33,7 @@ import org.springframework.boot.ansi.AnsiColor; import org.springframework.boot.ansi.AnsiColors; import org.springframework.boot.ansi.AnsiElement; import org.springframework.boot.ansi.AnsiOutput; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.core.env.Environment; -import org.springframework.core.env.PropertyResolver; import org.springframework.core.io.Resource; import org.springframework.util.Assert; @@ -45,6 +43,7 @@ import org.springframework.util.Assert; * * @author Craig Burke * @author Phillip Webb + * @author Madhura Bhave * @since 1.4.0 */ public class ImageBanner implements Banner { @@ -92,12 +91,11 @@ public class ImageBanner implements Banner { private void printBanner(Environment environment, PrintStream out) throws IOException { - PropertyResolver properties = new RelaxedPropertyResolver(environment, - "banner.image."); - int width = properties.getProperty("width", Integer.class, 76); - int height = properties.getProperty("height", Integer.class, 0); - int margin = properties.getProperty("margin", Integer.class, 2); - boolean invert = properties.getProperty("invert", Boolean.class, false); + int width = environment.getProperty("banner.image.width", Integer.class, 76); + int height = environment.getProperty("banner.image.height", Integer.class, 0); + int margin = environment.getProperty("banner.image.margin", Integer.class, 2); + boolean invert = environment.getProperty("banner.image.invert", Boolean.class, + false); BufferedImage image = readImage(width, height); printBanner(image, margin, invert, out); } diff --git a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 823c77310e..4a8e28c3a9 100644 --- a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -40,8 +40,8 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.boot.Banner.Mode; -import org.springframework.boot.bind.PropertiesConfigurationFactory; -import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; @@ -55,8 +55,6 @@ import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.GenericTypeResolver; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.env.CommandLinePropertySource; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; @@ -76,7 +74,6 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StopWatch; import org.springframework.util.StringUtils; -import org.springframework.validation.BindException; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.StandardServletEnvironment; @@ -578,9 +575,8 @@ public class SpringApplication { private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) { if (System.getProperty( CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, - "spring.beaninfo."); - Boolean ignore = resolver.getProperty("ignore", Boolean.class, Boolean.TRUE); + Boolean ignore = environment.getProperty("spring.beaninfo.ignore", + Boolean.class, Boolean.TRUE); System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString()); } @@ -591,16 +587,10 @@ public class SpringApplication { * @param environment the environment to bind */ protected void bindToSpringApplication(ConfigurableEnvironment environment) { - PropertiesConfigurationFactory binder = new PropertiesConfigurationFactory<>( - this); - ConversionService conversionService = new DefaultConversionService(); - binder.setTargetName("spring.main"); - binder.setConversionService(conversionService); - binder.setPropertySources(environment.getPropertySources()); try { - binder.bindPropertiesToTarget(); + Binder.get(environment).bind("spring.main", Bindable.ofInstance(this)); } - catch (BindException ex) { + catch (Exception ex) { throw new IllegalStateException("Cannot bind to SpringApplication", ex); } } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/FileEncodingApplicationListener.java b/spring-boot/src/main/java/org/springframework/boot/context/FileEncodingApplicationListener.java index 50e3d2d4e9..29f0e43684 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/FileEncodingApplicationListener.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/FileEncodingApplicationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -19,10 +19,10 @@ package org.springframework.boot.context; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; /** * An {@link ApplicationListener} that halts application startup if the system file @@ -42,6 +42,7 @@ import org.springframework.core.Ordered; * character-encoding value (e.g. "en_GB.UTF-8"). * * @author Dave Syer + * @author Madhura Bhave */ public class FileEncodingApplicationListener implements ApplicationListener, Ordered { @@ -56,26 +57,25 @@ public class FileEncodingApplicationListener @Override public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - event.getEnvironment(), "spring."); - if (resolver.containsProperty("mandatoryFileEncoding")) { - String encoding = System.getProperty("file.encoding"); - String desired = resolver.getProperty("mandatoryFileEncoding"); - if (encoding != null && !desired.equalsIgnoreCase(encoding)) { - logger.error("System property 'file.encoding' is currently '" + encoding - + "'. It should be '" + desired - + "' (as defined in 'spring.mandatoryFileEncoding')."); - logger.error("Environment variable LANG is '" + System.getenv("LANG") - + "'. You could use a locale setting that matches encoding='" - + desired + "'."); - logger.error("Environment variable LC_ALL is '" + System.getenv("LC_ALL") - + "'. You could use a locale setting that matches encoding='" - + desired + "'."); - throw new IllegalStateException( - "The Java Virtual Machine has not been configured to use the " - + "desired default character encoding (" + desired - + ")."); - } + ConfigurableEnvironment environment = event.getEnvironment(); + if (!environment.containsProperty("spring.mandatory-file-encoding")) { + return; + } + String encoding = System.getProperty("file.encoding"); + String desired = environment.getProperty("spring.mandatory-file-encoding"); + if (encoding != null && !desired.equalsIgnoreCase(encoding)) { + logger.error("System property 'file.encoding' is currently '" + encoding + + "'. It should be '" + desired + + "' (as defined in 'spring.mandatoryFileEncoding')."); + logger.error("Environment variable LANG is '" + System.getenv("LANG") + + "'. You could use a locale setting that matches encoding='" + + desired + "'."); + logger.error("Environment variable LC_ALL is '" + System.getenv("LC_ALL") + + "'. You could use a locale setting that matches encoding='" + + desired + "'."); + throw new IllegalStateException( + "The Java Virtual Machine has not been configured to use the " + + "desired default character encoding (" + desired + ")."); } } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/config/AnsiOutputApplicationListener.java b/spring-boot/src/main/java/org/springframework/boot/context/config/AnsiOutputApplicationListener.java index d82f1e2ef0..24bbe98a10 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/config/AnsiOutputApplicationListener.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/config/AnsiOutputApplicationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * 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. @@ -18,10 +18,11 @@ package org.springframework.boot.context.config; import org.springframework.boot.ansi.AnsiOutput; import org.springframework.boot.ansi.AnsiOutput.Enabled; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.ApplicationListener; import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; /** * An {@link ApplicationListener} that configures {@link AnsiOutput} depending on the @@ -29,6 +30,7 @@ import org.springframework.core.Ordered; * values. * * @author Raphael von der Grün + * @author Madhura Bhave * @since 1.2.0 */ public class AnsiOutputApplicationListener @@ -36,23 +38,17 @@ public class AnsiOutputApplicationListener @Override public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - event.getEnvironment(), "spring.output.ansi."); - if (resolver.containsProperty("enabled")) { - String enabled = resolver.getProperty("enabled"); - AnsiOutput.setEnabled(Enum.valueOf(Enabled.class, enabled.toUpperCase())); - } - - if (resolver.containsProperty("console-available")) { - AnsiOutput.setConsoleAvailable( - resolver.getProperty("console-available", Boolean.class)); - } + ConfigurableEnvironment environment = event.getEnvironment(); + Binder.get(environment) + .bind("spring.output.ansi.enabled", AnsiOutput.Enabled.class) + .ifBound(AnsiOutput::setEnabled); + AnsiOutput.setConsoleAvailable(environment + .getProperty("spring.output.ansi.console-available", Boolean.class)); } @Override public int getOrder() { - // Apply after ConfigFileApplicationListener has called all - // EnvironmentPostProcessors + // Apply after ConfigFileApplicationListener has called EnvironmentPostProcessors return ConfigFileApplicationListener.DEFAULT_ORDER + 1; } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java b/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java index b4120d2544..297ac295f0 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java @@ -34,10 +34,11 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.SpringApplication; -import org.springframework.boot.bind.PropertySourcesPropertyValues; -import org.springframework.boot.bind.RelaxedDataBinder; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.env.EnumerableCompositePropertySource; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.boot.env.PropertySourcesLoader; @@ -90,6 +91,7 @@ import org.springframework.util.StringUtils; * @author Stephane Nicoll * @author Andy Wilkinson * @author Eddú Meléndez + * @author Madhura Bhave */ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, ApplicationListener, Ordered { @@ -256,8 +258,7 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, } private void reorderSources(ConfigurableEnvironment environment) { - ConfigurationPropertySources - .finishAndRelocate(environment.getPropertySources()); + LoadedPropertySources.finishAndRelocate(environment.getPropertySources()); PropertySource defaultProperties = environment.getPropertySources() .remove(DEFAULT_PROPERTIES); if (defaultProperties != null) { @@ -344,11 +345,11 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, } // Any pre-existing active profiles set via property sources (e.g. System // properties) take precedence over those added in config files. - SpringProfiles springProfiles = bindSpringProfiles( - this.environment.getPropertySources()); - Set activeProfiles = new LinkedHashSet<>( - springProfiles.getActiveProfiles()); - activeProfiles.addAll(springProfiles.getIncludeProfiles()); + Set active = getProfiles(this.environment, "spring.profiles.active"); + Set activeProfiles = new LinkedHashSet<>(active); + Set include = getProfiles(this.environment, + "spring.profiles.include"); + activeProfiles.addAll(include); maybeActivateProfiles(activeProfiles); return activeProfiles; } @@ -474,33 +475,34 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, } private void handleProfileProperties(PropertySource propertySource) { - SpringProfiles springProfiles = bindSpringProfiles(propertySource); - maybeActivateProfiles(springProfiles.getActiveProfiles()); - addProfiles(springProfiles.getIncludeProfiles()); - } - - private SpringProfiles bindSpringProfiles(PropertySource propertySource) { MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(propertySource); - return bindSpringProfiles(propertySources); + Set active = getProfiles(propertySources, "spring.profiles.active"); + Set include = getProfiles(propertySources, + "spring.profiles.include"); + maybeActivateProfiles(active); + addProfiles(include); } - private SpringProfiles bindSpringProfiles(PropertySources propertySources) { - SpringProfiles springProfiles = new SpringProfiles(); - RelaxedDataBinder dataBinder = new RelaxedDataBinder(springProfiles, - "spring.profiles"); - dataBinder.bind(new PropertySourcesPropertyValues(propertySources, false)); - springProfiles.setActive(resolvePlaceholders(springProfiles.getActive())); - springProfiles.setInclude(resolvePlaceholders(springProfiles.getInclude())); - return springProfiles; + private Set getProfiles(ConfigurableEnvironment environment, + String name) { + return getProfiles(environment.getPropertySources(), name); } - private List resolvePlaceholders(List values) { - List resolved = new ArrayList<>(); - for (String value : values) { - resolved.add(this.environment.resolvePlaceholders(value)); + private Set getProfiles(PropertySources sources, String name) { + Binder binder = new Binder(ConfigurationPropertySources.get(sources), + new PropertySourcesPlaceholdersResolver(this.environment)); + return binder.bind(name, String[].class).map(this::asProfileSet) + .orElse(Collections.emptySet()); + } + + private Set asProfileSet(String[] profileNames) { + List profiles = new ArrayList<>(); + for (String profileName : profileNames) { + profiles.add(new Profile(profileName)); } - return resolved; + Collections.reverse(profiles); + return new LinkedHashSet<>(profiles); } private void maybeActivateProfiles(Set profiles) { @@ -601,19 +603,17 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, for (PropertySource item : sources) { reorderedSources.add(item); } - addConfigurationProperties( - new ConfigurationPropertySources(reorderedSources)); + addConfigurationProperties(new LoadedPropertySources(reorderedSources)); } - private void addConfigurationProperties( - ConfigurationPropertySources configurationSources) { + private void addConfigurationProperties(LoadedPropertySources loadedSources) { MutablePropertySources existingSources = this.environment .getPropertySources(); if (existingSources.contains(DEFAULT_PROPERTIES)) { - existingSources.addBefore(DEFAULT_PROPERTIES, configurationSources); + existingSources.addBefore(DEFAULT_PROPERTIES, loadedSources); } else { - existingSources.addLast(configurationSources); + existingSources.addLast(loadedSources); } } @@ -670,14 +670,14 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, * Holds the configuration {@link PropertySource}s as they are loaded can relocate * them once configuration classes have been processed. */ - static class ConfigurationPropertySources + static class LoadedPropertySources extends EnumerablePropertySource>> { private final Collection> sources; private final String[] names; - ConfigurationPropertySources(Collection> sources) { + LoadedPropertySources(Collection> sources) { super(APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME, sources); this.sources = sources; List names = new ArrayList<>(); @@ -703,7 +703,7 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, public static void finishAndRelocate(MutablePropertySources propertySources) { String name = APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME; - ConfigurationPropertySources removed = (ConfigurationPropertySources) propertySources + LoadedPropertySources removed = (LoadedPropertySources) propertySources .get(name); if (removed != null) { for (PropertySource propertySource : removed.sources) { @@ -729,48 +729,4 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, } - /** - * Holder for {@code spring.profiles} properties. - */ - static final class SpringProfiles { - - private List active = new ArrayList<>(); - - private List include = new ArrayList<>(); - - public List getActive() { - return this.active; - } - - public void setActive(List active) { - this.active = active; - } - - public List getInclude() { - return this.include; - } - - public void setInclude(List include) { - this.include = include; - } - - Set getActiveProfiles() { - return asProfileSet(this.active); - } - - Set getIncludeProfiles() { - return asProfileSet(this.include); - } - - private Set asProfileSet(List profileNames) { - List profiles = new ArrayList<>(); - for (String profileName : profileNames) { - profiles.add(new Profile(profileName)); - } - Collections.reverse(profiles); - return new LinkedHashSet<>(profiles); - } - - } - } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java b/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java index 5292b86689..1f52d7282b 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java @@ -16,9 +16,9 @@ package org.springframework.boot.context.logging; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; @@ -26,11 +26,12 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.SpringApplication; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.boot.context.event.ApplicationStartingEvent; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggingInitializationContext; @@ -77,11 +78,15 @@ import org.springframework.util.StringUtils; * @author Dave Syer * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave * @since 2.0.0 * @see LoggingSystem#get(ClassLoader) */ public class LoggingApplicationListener implements GenericApplicationListener { + private static final Bindable> STRING_STRING_MAP = Bindable + .mapOf(String.class, String.class); + /** * The default order for the LoggingApplicationListener. */ @@ -294,20 +299,18 @@ public class LoggingApplicationListener implements GenericApplicationListener { } protected void setLogLevels(LoggingSystem system, Environment environment) { - Map levels = new RelaxedPropertyResolver(environment) - .getSubProperties("logging.level."); - for (Entry entry : levels.entrySet()) { - setLogLevel(system, environment, entry.getKey(), entry.getValue().toString()); + if (!(environment instanceof ConfigurableEnvironment)) { + return; } + Binder binder = Binder.get(environment); + binder.bind("logging.level", STRING_STRING_MAP).orElseGet(Collections::emptyMap) + .forEach((name, level) -> setLogLevel(system, environment, name, level)); } private void setLogLevel(LoggingSystem system, Environment environment, String name, String level) { try { - if (name.equalsIgnoreCase(LoggingSystem.ROOT_LOGGER_NAME)) { - name = null; - } - level = environment.resolvePlaceholders(level); + name = (name.equalsIgnoreCase(LoggingSystem.ROOT_LOGGER_NAME) ? null : name); system.setLogLevel(name, coerceLogLevel(level)); } catch (RuntimeException ex) { @@ -324,7 +327,7 @@ public class LoggingApplicationListener implements GenericApplicationListener { private void registerShutdownHookIfNecessary(Environment environment, LoggingSystem loggingSystem) { - boolean registerShutdownHook = new RelaxedPropertyResolver(environment) + boolean registerShutdownHook = environment .getProperty(REGISTER_SHUTDOWN_HOOK_PROPERTY, Boolean.class, false); if (registerShutdownHook) { Runnable shutdownHandler = loggingSystem.getShutdownHandler(); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java index c1e358d1df..7d718f7f8e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -17,7 +17,6 @@ package org.springframework.boot.context.properties; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; @@ -34,7 +33,15 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.boot.bind.PropertiesConfigurationFactory; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver; +import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler; +import org.springframework.boot.context.properties.bind.handler.IgnoreNestedPropertiesBindHandler; +import org.springframework.boot.context.properties.bind.handler.NoUnboundElementsBindHandler; +import org.springframework.boot.context.properties.bind.validation.ValidationBindHandler; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.validation.MessageInterpolatorFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -53,13 +60,10 @@ import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.core.env.StandardEnvironment; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; @@ -73,6 +77,7 @@ import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; * @author Phillip Webb * @author Christian Dupuis * @author Stephane Nicoll + * @author Madhura Bhave */ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, BeanFactoryAware, EnvironmentAware, ApplicationContextAware, InitializingBean, @@ -113,6 +118,10 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc private int order = Ordered.HIGHEST_PRECEDENCE + 1; + private ConfigurationPropertySources configurationSources; + + private Binder binder; + /** * A list of custom converters (in addition to the defaults) to use when converting * properties for binding. @@ -212,6 +221,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME, ConversionService.class); } + this.configurationSources = ConfigurationPropertySources + .get(this.propertySources); } @Override @@ -240,18 +251,13 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc private PropertySources deducePropertySources() { PropertySourcesPlaceholderConfigurer configurer = getSinglePropertySourcesPlaceholderConfigurer(); if (configurer != null) { - // Flatten the sources into a single list so they can be iterated - return new FlatPropertySources(configurer.getAppliedPropertySources()); + return configurer.getAppliedPropertySources(); } if (this.environment instanceof ConfigurableEnvironment) { - MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment) - .getPropertySources(); - return new FlatPropertySources(propertySources); + return ((ConfigurableEnvironment) this.environment).getPropertySources(); } - // empty, so not very useful, but fulfils the contract - logger.warn("Unable to obtain PropertySources from " + throw new IllegalStateException("Unable to obtain PropertySources from " + "PropertySourcesPlaceholderConfigurer or Environment"); - return new MutablePropertySources(); } private PropertySourcesPlaceholderConfigurer getSinglePropertySourcesPlaceholderConfigurer() { @@ -287,15 +293,16 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc throws BeansException { ConfigurationProperties annotation = AnnotationUtils .findAnnotation(bean.getClass(), ConfigurationProperties.class); + Object bound = bean; if (annotation != null) { - postProcessBeforeInitialization(bean, beanName, annotation); + bound = postProcessBeforeInitialization(bean, beanName, annotation); } annotation = this.beans.findFactoryAnnotation(beanName, ConfigurationProperties.class); if (annotation != null) { - postProcessBeforeInitialization(bean, beanName, annotation); + bound = postProcessBeforeInitialization(bean, beanName, annotation); } - return bean; + return bound; } @Override @@ -304,35 +311,53 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc return bean; } - private void postProcessBeforeInitialization(Object bean, String beanName, + private Object postProcessBeforeInitialization(Object bean, String beanName, ConfigurationProperties annotation) { - Object target = bean; - PropertiesConfigurationFactory factory = new PropertiesConfigurationFactory<>( - target); - factory.setPropertySources(this.propertySources); - factory.setValidator(determineValidator(bean)); - // If no explicit conversion service is provided we add one so that (at least) - // comma-separated arrays of convertibles can be bound automatically - factory.setConversionService(this.conversionService == null - ? getDefaultConversionService() : this.conversionService); - if (annotation != null) { - factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields()); - factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields()); - factory.setIgnoreNestedProperties(annotation.ignoreNestedProperties()); - if (StringUtils.hasLength(annotation.prefix())) { - factory.setTargetName(annotation.prefix()); - } - } + Binder binder = getBinder(); + Validator validator = determineValidator(bean); + BindHandler handler = getBindHandler(annotation, validator); + Bindable bindable = Bindable.ofInstance(bean); try { - factory.bindPropertiesToTarget(); + binder.bind(annotation.prefix(), bindable, handler); + return bean; } catch (Exception ex) { - String targetClass = ClassUtils.getShortName(target.getClass()); + String targetClass = ClassUtils.getShortName(bean.getClass()); throw new BeanCreationException(beanName, "Could not bind properties to " + targetClass + " (" + getAnnotationDetails(annotation) + ")", ex); } } + private Binder getBinder() { + Binder binder = this.binder; + if (binder == null) { + ConversionService conversionService = this.conversionService; + if (conversionService == null) { + conversionService = getDefaultConversionService(); + } + binder = new Binder(this.configurationSources, + new PropertySourcesPlaceholdersResolver(this.propertySources), + conversionService); + this.binder = binder; + } + return binder; + } + + private ConversionService getDefaultConversionService() { + if (this.defaultConversionService == null) { + DefaultConversionService conversionService = new DefaultConversionService(); + this.applicationContext.getAutowireCapableBeanFactory().autowireBean(this); + for (Converter converter : this.converters) { + conversionService.addConverter(converter); + } + for (GenericConverter genericConverter : this.genericConverters) { + conversionService.addConverter(genericConverter); + } + this.defaultConversionService = conversionService; + } + return this.defaultConversionService; + } + private String getAnnotationDetails(ConfigurationProperties annotation) { if (annotation == null) { return ""; @@ -379,19 +404,22 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc return true; } - private ConversionService getDefaultConversionService() { - if (this.defaultConversionService == null) { - DefaultConversionService conversionService = new DefaultConversionService(); - this.applicationContext.getAutowireCapableBeanFactory().autowireBean(this); - for (Converter converter : this.converters) { - conversionService.addConverter(converter); - } - for (GenericConverter genericConverter : this.genericConverters) { - conversionService.addConverter(genericConverter); - } - this.defaultConversionService = conversionService; + private BindHandler getBindHandler(ConfigurationProperties annotation, + Validator validator) { + BindHandler handler = BindHandler.DEFAULT; + if (annotation.ignoreInvalidFields()) { + handler = new IgnoreErrorsBindHandler(handler); } - return this.defaultConversionService; + if (!annotation.ignoreUnknownFields()) { + handler = new NoUnboundElementsBindHandler(handler); + } + if (annotation.ignoreNestedProperties()) { + handler = new IgnoreNestedPropertiesBindHandler(handler); + } + if (validator != null) { + handler = new ValidationBindHandler(handler, validator); + } + return handler; } /** @@ -465,56 +493,4 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc } - /** - * Convenience class to flatten out a tree of property sources without losing the - * reference to the backing data (which can therefore be updated in the background). - */ - private static class FlatPropertySources implements PropertySources { - - private PropertySources propertySources; - - FlatPropertySources(PropertySources propertySources) { - this.propertySources = propertySources; - } - - @Override - public Iterator> iterator() { - MutablePropertySources result = getFlattened(); - return result.iterator(); - } - - @Override - public boolean contains(String name) { - return get(name) != null; - } - - @Override - public PropertySource get(String name) { - return getFlattened().get(name); - } - - private MutablePropertySources getFlattened() { - MutablePropertySources result = new MutablePropertySources(); - for (PropertySource propertySource : this.propertySources) { - flattenPropertySources(propertySource, result); - } - return result; - } - - private void flattenPropertySources(PropertySource propertySource, - MutablePropertySources result) { - Object source = propertySource.getSource(); - if (source instanceof ConfigurableEnvironment) { - ConfigurableEnvironment environment = (ConfigurableEnvironment) source; - for (PropertySource childSource : environment.getPropertySources()) { - flattenPropertySources(childSource, result); - } - } - else { - result.addLast(propertySource); - } - } - - } - } diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java b/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java index da7c9e16c1..5794911ff9 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java @@ -17,8 +17,10 @@ package org.springframework.boot.logging; import org.springframework.boot.ApplicationPid; -import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.PropertySourcesPropertyResolver; import org.springframework.util.Assert; /** @@ -26,6 +28,7 @@ import org.springframework.util.Assert; * * @author Andy Wilkinson * @author Phillip Webb + * @author Madhura Bhave * @since 2.0.0 */ public class LoggingSystemProperties { @@ -81,22 +84,33 @@ public class LoggingSystemProperties { } public void apply(LogFile logFile) { - RelaxedPropertyResolver propertyResolver = RelaxedPropertyResolver - .ignoringUnresolvableNestedPlaceholders(this.environment, "logging."); - setSystemProperty(propertyResolver, EXCEPTION_CONVERSION_WORD, + PropertyResolver resolver = getPropertyResolver(); + setSystemProperty(resolver, EXCEPTION_CONVERSION_WORD, "exception-conversion-word"); - setSystemProperty(propertyResolver, CONSOLE_LOG_PATTERN, "pattern.console"); - setSystemProperty(propertyResolver, FILE_LOG_PATTERN, "pattern.file"); - setSystemProperty(propertyResolver, LOG_LEVEL_PATTERN, "pattern.level"); + setSystemProperty(resolver, CONSOLE_LOG_PATTERN, "pattern.console"); + setSystemProperty(resolver, FILE_LOG_PATTERN, "pattern.file"); + setSystemProperty(resolver, LOG_LEVEL_PATTERN, "pattern.level"); setSystemProperty(PID_KEY, new ApplicationPid().toString()); if (logFile != null) { logFile.applyToSystemProperties(); } } - private void setSystemProperty(RelaxedPropertyResolver propertyResolver, - String systemPropertyName, String propertyName) { - setSystemProperty(systemPropertyName, propertyResolver.getProperty(propertyName)); + private PropertyResolver getPropertyResolver() { + if (this.environment instanceof ConfigurableEnvironment) { + PropertyResolver resolver = new PropertySourcesPropertyResolver( + ((ConfigurableEnvironment) this.environment).getPropertySources()); + ((PropertySourcesPropertyResolver) resolver) + .setIgnoreUnresolvableNestedPlaceholders(true); + return resolver; + } + return this.environment; + } + + private void setSystemProperty(PropertyResolver resolver, String systemPropertyName, + String propertyName) { + setSystemProperty(systemPropertyName, + resolver.getProperty("logging." + propertyName)); } private void setSystemProperty(String name, String value) { diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java b/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java index ec1451bfb1..17b92c6f24 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java @@ -30,9 +30,9 @@ import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; import ch.qos.logback.core.util.FileSize; import ch.qos.logback.core.util.OptionHelper; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LoggingInitializationContext; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertyResolver; import org.springframework.core.env.PropertySourcesPropertyResolver; @@ -44,6 +44,7 @@ import org.springframework.util.ReflectionUtils; * and {@code file-appender.xml} files provided for classic {@code logback.xml} use. * * @author Phillip Webb + * @author Madhura Bhave * @since 1.1.2 */ class DefaultLogbackConfiguration { @@ -72,8 +73,13 @@ class DefaultLogbackConfiguration { if (environment == null) { return new PropertySourcesPropertyResolver(null); } - return RelaxedPropertyResolver.ignoringUnresolvableNestedPlaceholders(environment, - "logging.pattern."); + if (environment instanceof ConfigurableEnvironment) { + PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver( + ((ConfigurableEnvironment) environment).getPropertySources()); + resolver.setIgnoreUnresolvableNestedPlaceholders(true); + return resolver; + } + return environment; } public void apply(LogbackConfigurator config) { @@ -113,7 +119,8 @@ class DefaultLogbackConfiguration { private Appender consoleAppender(LogbackConfigurator config) { ConsoleAppender appender = new ConsoleAppender<>(); PatternLayoutEncoder encoder = new PatternLayoutEncoder(); - String logPattern = this.patterns.getProperty("console", CONSOLE_LOG_PATTERN); + String logPattern = this.patterns.getProperty("logging.pattern.console", + CONSOLE_LOG_PATTERN); encoder.setPattern(OptionHelper.substVars(logPattern, config.getContext())); encoder.setCharset(UTF8); config.start(encoder); @@ -126,7 +133,8 @@ class DefaultLogbackConfiguration { String logFile) { RollingFileAppender appender = new RollingFileAppender<>(); PatternLayoutEncoder encoder = new PatternLayoutEncoder(); - String logPattern = this.patterns.getProperty("file", FILE_LOG_PATTERN); + String logPattern = this.patterns.getProperty("logging.pattern.file", + FILE_LOG_PATTERN); encoder.setPattern(OptionHelper.substVars(logPattern, config.getContext())); appender.setEncoder(encoder); config.start(encoder); diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringPropertyAction.java b/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringPropertyAction.java index 4b9f67251d..ca64626793 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringPropertyAction.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringPropertyAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -24,7 +24,6 @@ import ch.qos.logback.core.joran.spi.InterpretationContext; import ch.qos.logback.core.util.OptionHelper; import org.xml.sax.Attributes; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.core.env.Environment; /** @@ -33,6 +32,7 @@ import org.springframework.core.env.Environment; * * @author Phillip Webb * @author Eddú Meléndez + * @author Madhura Bhave */ class SpringPropertyAction extends Action { @@ -47,8 +47,8 @@ class SpringPropertyAction extends Action { } @Override - public void begin(InterpretationContext ic, String elementName, Attributes attributes) - throws ActionException { + public void begin(InterpretationContext context, String elementName, + Attributes attributes) throws ActionException { String name = attributes.getValue(NAME_ATTRIBUTE); String source = attributes.getValue(SOURCE_ATTRIBUTE); Scope scope = ActionUtil.stringToScope(attributes.getValue(SCOPE_ATTRIBUTE)); @@ -57,7 +57,7 @@ class SpringPropertyAction extends Action { addError( "The \"name\" and \"source\" attributes of must be set"); } - ActionUtil.setProperty(ic, name, getValue(source, defaultValue), scope); + ActionUtil.setProperty(context, name, getValue(source, defaultValue), scope); } private String getValue(String source, String defaultValue) { @@ -72,15 +72,14 @@ class SpringPropertyAction extends Action { int lastDot = source.lastIndexOf("."); if (lastDot > 0) { String prefix = source.substring(0, lastDot + 1); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - this.environment, prefix); - return resolver.getProperty(source.substring(lastDot + 1), defaultValue); + return this.environment.getProperty(prefix + source.substring(lastDot + 1), + defaultValue); } return defaultValue; } @Override - public void end(InterpretationContext ic, String name) throws ActionException { + public void end(InterpretationContext context, String name) throws ActionException { } } diff --git a/spring-boot/src/main/java/org/springframework/boot/system/ApplicationPidFileWriter.java b/spring-boot/src/main/java/org/springframework/boot/system/ApplicationPidFileWriter.java index afb0a2f565..726a32a06e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/system/ApplicationPidFileWriter.java +++ b/spring-boot/src/main/java/org/springframework/boot/system/ApplicationPidFileWriter.java @@ -27,7 +27,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.boot.ApplicationPid; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -57,6 +56,7 @@ import org.springframework.util.Assert; * @author Dave Syer * @author Phillip Webb * @author Tomasz Przybyla + * @author Madhura Bhave * @since 1.4.0 */ public class ApplicationPidFileWriter @@ -221,8 +221,7 @@ public class ApplicationPidFileWriter if (environment == null) { return null; } - return new RelaxedPropertyResolver(environment, this.prefix) - .getProperty(this.key); + return environment.getProperty(this.prefix + this.key); } private Environment getEnvironment(SpringApplicationEvent event) { diff --git a/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java b/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java index b197ef033e..5a5aa380f1 100644 --- a/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java +++ b/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java @@ -25,14 +25,12 @@ import java.util.List; import java.util.Properties; import java.util.Set; -import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.config.YamlProcessor.DocumentMatcher; import org.springframework.beans.factory.config.YamlProcessor.MatchStatus; -import org.springframework.boot.bind.PropertySourcesPropertyValues; -import org.springframework.boot.bind.RelaxedDataBinder; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.core.env.Environment; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertiesPropertySource; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -47,6 +45,7 @@ import org.springframework.util.StringUtils; * @author Matt Benson * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave */ public class SpringProfileDocumentMatcher implements DocumentMatcher { @@ -69,14 +68,9 @@ public class SpringProfileDocumentMatcher implements DocumentMatcher { } protected List extractSpringProfiles(Properties properties) { - SpringProperties springProperties = new SpringProperties(); - MutablePropertySources propertySources = new MutablePropertySources(); - propertySources.addFirst(new PropertiesPropertySource("profiles", properties)); - PropertyValues propertyValues = new PropertySourcesPropertyValues( - propertySources); - new RelaxedDataBinder(springProperties, "spring").bind(propertyValues); - List profiles = springProperties.getProfiles(); - return profiles; + Binder binder = new Binder(new MapConfigurationPropertySource(properties)); + return binder.bind("spring.profiles", Bindable.of(String[].class)) + .map(Arrays::asList).orElse(Collections.emptyList()); } private MatchStatus matches(List profiles) { diff --git a/spring-boot/src/test/java/org/springframework/boot/context/FileEncodingApplicationListenerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/FileEncodingApplicationListenerTests.java index b698a32bd7..415a516238 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/FileEncodingApplicationListenerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/FileEncodingApplicationListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -21,6 +21,7 @@ import org.junit.Test; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.StandardEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; @@ -43,6 +44,7 @@ public class FileEncodingApplicationListenerTests { public void testIllegalState() { TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, "spring.mandatory_file_encoding=FOO"); + ConfigurationPropertySources.attach(this.environment); this.initializer.onApplicationEvent(this.event); } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java index aaacefae5d..11aa788a2d 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java @@ -40,7 +40,7 @@ import org.junit.rules.ExpectedException; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; -import org.springframework.boot.context.config.ConfigFileApplicationListener.ConfigurationPropertySources; +import org.springframework.boot.context.config.ConfigFileApplicationListener.LoadedPropertySources; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.boot.env.EnumerableCompositePropertySource; @@ -512,7 +512,7 @@ public class ConfigFileApplicationListenerTests { String property = this.environment.getProperty("my.property"); assertThat(this.environment.getActiveProfiles()).contains("dev"); assertThat(property).isEqualTo("fromdevprofile"); - ConfigurationPropertySources propertySource = (ConfigurationPropertySources) this.environment + LoadedPropertySources propertySource = (LoadedPropertySources) this.environment .getPropertySources() .get(ConfigFileApplicationListener.APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME); Collection> sources = propertySource @@ -848,7 +848,7 @@ public class ConfigFileApplicationListenerTests { public boolean matches(ConfigurableEnvironment value) { MutablePropertySources sources = new MutablePropertySources( value.getPropertySources()); - ConfigurationPropertySources.finishAndRelocate(sources); + LoadedPropertySources.finishAndRelocate(sources); return sources.contains(sourceName); } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java index 5e6222c615..6e413b084e 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java @@ -41,6 +41,7 @@ import org.springframework.boot.ApplicationPid; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.context.event.ApplicationStartingEvent; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.junit.runner.classpath.ClassPathExclusions; import org.springframework.boot.junit.runner.classpath.ModifiedClassPathRunner; import org.springframework.boot.logging.AbstractLoggingSystem; @@ -56,6 +57,7 @@ import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.SimpleApplicationEventMulticaster; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.test.util.ReflectionTestUtils; @@ -72,7 +74,6 @@ import static org.hamcrest.Matchers.not; * @author Stephane Nicoll * @author Ben Hale */ - @RunWith(ModifiedClassPathRunner.class) @ClassPathExclusions("log4j*.jar") public class LoggingApplicationListenerTests { @@ -103,6 +104,8 @@ public class LoggingApplicationListenerTests { multicastEvent(new ApplicationStartingEvent(new SpringApplication(), NO_ARGS)); new File("target/foo.log").delete(); new File(tmpDir() + "/spring.log").delete(); + ConfigurableEnvironment environment = this.context.getEnvironment(); + ConfigurationPropertySources.attach(environment.getPropertySources()); } @After diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java index 5e81e4543f..18838f6c06 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java @@ -34,7 +34,8 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.boot.bind.RelaxedBindingNotWritablePropertyException; +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; import org.springframework.boot.testutil.InternalOutputCapture; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -43,7 +44,6 @@ import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.validation.BindException; import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; @@ -59,6 +59,7 @@ import static org.junit.Assert.fail; * @author Christian Dupuis * @author Phillip Webb * @author Stephane Nicoll + * @author Madhura Bhave */ public class ConfigurationPropertiesBindingPostProcessorTests { @@ -83,7 +84,16 @@ public class ConfigurationPropertiesBindingPostProcessorTests { TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, "test.foo=spam"); this.context.register(TestConfigurationWithValidatingSetter.class); - assertBindingFailure(1); + try { + this.context.refresh(); + fail("Expected exception"); + } + catch (BeanCreationException ex) { + BindException bindException = (BindException) ex.getCause(); + assertThat(bindException.getMessage()) + .startsWith("Failed to bind properties under 'test' to " + + PropertyWithValidatingSetter.class.getName()); + } } @Test @@ -97,12 +107,9 @@ public class ConfigurationPropertiesBindingPostProcessorTests { fail("Expected exception"); } catch (BeanCreationException ex) { - RelaxedBindingNotWritablePropertyException bex = (RelaxedBindingNotWritablePropertyException) ex - .getRootCause(); - assertThat(bex.getMessage()) - .startsWith("Failed to bind 'com.example.baz' from '" - + TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME - + "' to 'baz' " + "property on '" + BindException bindException = (BindException) ex.getCause(); + assertThat(bindException.getMessage()) + .startsWith("Failed to bind properties under 'com.example' to " + TestConfiguration.class.getName()); } } @@ -190,9 +197,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests { @Test public void testRelaxedPropertyWithEnum() throws Exception { doEnumTest("test.the-value=FoO"); - doEnumTest("TEST_THE_VALUE=FoO"); doEnumTest("test.THE_VALUE=FoO"); - doEnumTest("test_the_value=FoO"); } private void doEnumTest(String property) { @@ -209,8 +214,6 @@ public class ConfigurationPropertiesBindingPostProcessorTests { public void testRelaxedPropertyWithSetOfEnum() { doEnumSetTest("test.the-values=foo,bar", FooEnum.FOO, FooEnum.BAR); doEnumSetTest("test.the-values=foo", FooEnum.FOO); - doEnumSetTest("TEST_THE_VALUES=FoO", FooEnum.FOO); - doEnumSetTest("test_the_values=BaR,FoO", FooEnum.BAR, FooEnum.FOO); } private void doEnumSetTest(String property, FooEnum... expected) { @@ -266,17 +269,6 @@ public class ConfigurationPropertiesBindingPostProcessorTests { .isEqualTo("word".toCharArray()); } - @Test - public void configurationPropertiesWithArrayExpansion() throws Exception { - this.context = new AnnotationConfigApplicationContext(); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "test.chars[4]=s"); - this.context.register(PropertyWithCharArrayExpansion.class); - this.context.refresh(); - assertThat(this.context.getBean(PropertyWithCharArrayExpansion.class).getChars()) - .isEqualTo("words".toCharArray()); - } - @Test public void notWritablePropertyException() throws Exception { this.context = new AnnotationConfigApplicationContext(); @@ -294,12 +286,6 @@ public class ConfigurationPropertiesBindingPostProcessorTests { "test.BAR-B-A-Z=testa", "test.BAR-B-A-Z=testb"); } - @Test - public void relaxedPropertyNamesMixed() throws Exception { - testRelaxedPropertyNames("test.FOO_BAR=test2", "test.foo-bar=test1", - "test.BAR-B-A-Z=testb", "test.bar_b_a_z=testa"); - } - private void testRelaxedPropertyNames(String... environment) { this.context = new AnnotationConfigApplicationContext(); TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, @@ -316,7 +302,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests { // gh-3539 this.context = new AnnotationConfigApplicationContext(); TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "TEST_NESTED_VALUE=test1"); + "test.nested.value=test1"); this.context.register(PropertyWithNestedValue.class); this.context.refresh(); assertThat(this.context.getBean(PropertyWithNestedValue.class).getNested() @@ -335,6 +321,27 @@ public class ConfigurationPropertiesBindingPostProcessorTests { this.context.refresh(); } + @Test + public void bindWithIgnoreInvalidFieldsAnnotation() { + this.context = new AnnotationConfigApplicationContext(); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, + "com.example.bar=spam"); + this.context.register(TestConfigurationWithIgnoreErrors.class); + this.context.refresh(); + assertThat(this.context.getBean(TestConfigurationWithIgnoreErrors.class).getBar()) + .isEqualTo(0); + } + + @Test + public void bindWithNoIgnoreInvalidFieldsAnnotation() { + this.context = new AnnotationConfigApplicationContext(); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, + "com.example.foo=hello"); + this.context.register(TestConfiguration.class); + this.thrown.expect(BeanCreationException.class); + this.context.refresh(); + } + @Test public void multiplePropertySourcesPlaceholderConfigurer() throws Exception { this.context = new AnnotationConfigApplicationContext(); @@ -361,8 +368,8 @@ public class ConfigurationPropertiesBindingPostProcessorTests { fail("Expected exception"); } catch (BeanCreationException ex) { - BindException bex = (BindException) ex.getRootCause(); - assertThat(bex.getErrorCount()).isEqualTo(errorCount); + assertThat(((BindValidationException) ex.getRootCause()).getValidationErrors() + .getAllErrors().size()).isEqualTo(errorCount); } } @@ -407,6 +414,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests { } @ConfigurationProperties(prefix = "test") + @Validated public static class PropertyWithoutJSR303 implements Validator { private String foo; @@ -469,6 +477,8 @@ public class ConfigurationPropertiesBindingPostProcessorTests { @ConfigurationProperties(prefix = "com.example", ignoreUnknownFields = false) public static class TestConfiguration { + private int foo; + private String bar; public void setBar(String bar) { @@ -479,6 +489,30 @@ public class ConfigurationPropertiesBindingPostProcessorTests { return this.bar; } + public int getFoo() { + return this.foo; + } + + public void setFoo(int foo) { + this.foo = foo; + } + } + + @Configuration + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "com.example", ignoreInvalidFields = true) + public static class TestConfigurationWithIgnoreErrors { + + private long bar; + + public void setBar(long bar) { + this.bar = bar; + } + + public long getBar() { + return this.bar; + } + } @ConfigurationProperties(prefix = "test") @@ -549,6 +583,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests { } @ConfigurationProperties(prefix = "custom") + @Validated public static class PropertyWithCustomValidator { private String foo; @@ -647,6 +682,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests { @Configuration @EnableConfigurationProperties @ConfigurationProperties(prefix = "test") + @Validated public static class PropertyWithValue { @Value("${default.value}") @@ -669,6 +705,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests { @Configuration @EnableConfigurationProperties + @Validated @ConfigurationProperties(prefix = "test") public static class PropertiesWithMap { diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java index 3f3b16befc..504aec76d3 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java @@ -30,6 +30,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.bind.BindException; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -37,7 +38,6 @@ import org.springframework.context.annotation.ImportResource; import org.springframework.core.env.MutablePropertySources; import org.springframework.stereotype.Component; import org.springframework.test.context.support.TestPropertySourceUtils; -import org.springframework.validation.BindException; import org.springframework.validation.annotation.Validated; import static org.assertj.core.api.Assertions.assertThat; @@ -47,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Dave Syer * @author Stephane Nicoll + * @author Madhura Bhave */ public class EnableConfigurationPropertiesTests { @@ -94,30 +95,6 @@ public class EnableConfigurationPropertiesTests { .isEqualTo("bar"); } - @Test - public void testNestedSystemPropertiesBindingWithUnderscore() { - this.context.register(NestedConfiguration.class); - System.setProperty("name", "foo"); - System.setProperty("nested_name", "bar"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(NestedProperties.class)).hasSize(1); - assertThat(this.context.getBean(NestedProperties.class).name).isEqualTo("foo"); - assertThat(this.context.getBean(NestedProperties.class).nested.name) - .isEqualTo("bar"); - } - - @Test - public void testNestedOsEnvironmentVariableWithUnderscore() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "NAME=foo", "NESTED_NAME=bar"); - this.context.register(NestedConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(NestedProperties.class)).hasSize(1); - assertThat(this.context.getBean(NestedProperties.class).name).isEqualTo("foo"); - assertThat(this.context.getBean(NestedProperties.class).nested.name) - .isEqualTo("bar"); - } - @Test public void testStrictPropertiesBinding() { removeSystemProperties(); @@ -134,18 +111,7 @@ public class EnableConfigurationPropertiesTests { public void testPropertiesEmbeddedBinding() { this.context.register(EmbeddedTestConfiguration.class); TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "spring_foo_name=foo"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(EmbeddedTestProperties.class)) - .hasSize(1); - assertThat(this.context.getBean(TestProperties.class).name).isEqualTo("foo"); - } - - @Test - public void testOsEnvironmentVariableEmbeddedBinding() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "SPRING_FOO_NAME=foo"); - this.context.register(EmbeddedTestConfiguration.class); + "spring.foo.name=foo"); this.context.refresh(); assertThat(this.context.getBeanNamesForType(EmbeddedTestProperties.class)) .hasSize(1); @@ -322,15 +288,6 @@ public class EnableConfigurationPropertiesTests { assertThat(this.context.getBean(TestConsumer.class).getName()).isEqualTo("foo"); } - @Test - public void testUnderscoresInPrefix() throws Exception { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "spring_test_external_val=baz"); - this.context.register(SystemExampleConfig.class); - this.context.refresh(); - assertThat(this.context.getBean(SystemEnvVar.class).getVal()).isEqualTo("baz"); - } - @Test public void testSimpleAutoConfig() throws Exception { TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, @@ -623,7 +580,7 @@ public class EnableConfigurationPropertiesTests { private int[] array; - private final List list = new ArrayList<>(); + private List list = new ArrayList<>(); // No getter - you should be able to bind to a write-only bean @@ -643,6 +600,10 @@ public class EnableConfigurationPropertiesTests { return this.list; } + public void setList(List list) { + this.list = list; + } + } @ConfigurationProperties(ignoreUnknownFields = false) diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java index deeed6601e..76434a558d 100644 --- a/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -28,6 +28,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.impl.StaticLoggerBinder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.testutil.InternalOutputCapture; import org.springframework.mock.env.MockEnvironment; @@ -139,6 +140,7 @@ public class SpringBootJoranConfiguratorTests { public void relaxedSpringProperty() throws Exception { TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, "my.EXAMPLE_PROPERTY=test"); + ConfigurationPropertySources.attach(this.environment); initialize("property.xml"); assertThat(this.context.getProperty("MINE")).isEqualTo("test"); } diff --git a/spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java b/spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java index 4f867ee370..7ceaeff8d3 100644 --- a/spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -39,7 +39,7 @@ public class SpringProfileDocumentMatcherTests { @Test public void matchesSingleProfile() throws IOException { DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); - Properties properties = getProperties("spring.profiles: foo"); + Properties properties = getProperties("spring.ProfILEs: foo"); assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); } From c0d4a771c9124be0ee858686d419f89fbc83168e Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 25 Apr 2017 10:53:25 -0700 Subject: [PATCH 08/19] Use new configuration properties in spring-boot-test Update `spring-boot-test` to use the new configuration properties support. See gh-9000 --- .../test/context/SpringBootContextLoader.java | 28 +++++------ .../SpringBootTestContextBootstrapper.java | 48 ++++++------------- .../client/LocalHostUriTemplateHandler.java | 9 ++-- 3 files changed, 30 insertions(+), 55 deletions(-) diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java index 9c685f77de..c984c2a9a6 100644 --- a/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java @@ -21,13 +21,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Set; import org.springframework.beans.BeanUtils; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; -import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.boot.test.mock.web.SpringBootMockServletContext; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext; @@ -40,10 +41,6 @@ import org.springframework.core.SpringVersion; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.Order; import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertySources; -import org.springframework.core.env.PropertySourcesPropertyResolver; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.test.context.ContextConfigurationAttributes; @@ -76,6 +73,7 @@ import org.springframework.web.context.support.GenericWebApplicationContext; * @author Phillip Webb * @author Andy Wilkinson * @author Stephane Nicoll + * @author Madhura Bhave * @see SpringBootTest */ public class SpringBootContextLoader extends AbstractContextLoader { @@ -171,19 +169,15 @@ public class SpringBootContextLoader extends AbstractContextLoader { } private boolean hasCustomServerPort(List properties) { - PropertySources sources = convertToPropertySources(properties); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - new PropertySourcesPropertyResolver(sources), "server."); - return resolver.containsProperty("port"); + Binder binder = new Binder(convertToConfigurationPropertySource(properties)); + return binder.bind("server.port", Bindable.of(String.class)).isBound(); } - private PropertySources convertToPropertySources(List properties) { - Map source = TestPropertySourceUtils - .convertInlinedPropertiesToMap( - properties.toArray(new String[properties.size()])); - MutablePropertySources sources = new MutablePropertySources(); - sources.addFirst(new MapPropertySource("inline", source)); - return sources; + private MapConfigurationPropertySource convertToConfigurationPropertySource( + List properties) { + String[] array = properties.toArray(new String[properties.size()]); + return new MapConfigurationPropertySource( + TestPropertySourceUtils.convertInlinedPropertiesToMap(array)); } private List> getInitializers( diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java index 96d70f16b7..3ce6e5f1c6 100644 --- a/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; @@ -29,15 +28,14 @@ import org.apache.commons.logging.LogFactory; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.WebApplicationType; -import org.springframework.boot.bind.RelaxedPropertyResolver; +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.MapConfigurationPropertySource; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; 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.core.env.PropertySourcesPropertyResolver; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfigurationAttributes; @@ -71,6 +69,7 @@ import org.springframework.util.ObjectUtils; * @author Phillip Webb * @author Andy Wilkinson * @author Brian Clozel + * @author Madhura Bhave * @since 1.4.0 * @see SpringBootTest * @see TestConfiguration @@ -98,7 +97,7 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr verifyConfiguration(context.getTestClass()); WebEnvironment webEnvironment = getWebEnvironment(context.getTestClass()); if (webEnvironment == WebEnvironment.MOCK - && deduceWebApplication() == WebApplicationType.SERVLET) { + && deduceWebApplicationType() == WebApplicationType.SERVLET) { context.setAttribute(ACTIVATE_SERVLET_LISTENER, true); } else if (webEnvironment != null && webEnvironment.isEmbedded()) { @@ -181,15 +180,17 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr private WebApplicationType getWebApplicationType( MergedContextConfiguration configuration) { - WebApplicationType webApplicationType = getConfiguredWebApplicationType( - configuration); - if (webApplicationType != null) { - return webApplicationType; - } - return deduceWebApplication(); + ConfigurationPropertySource source = new MapConfigurationPropertySource( + TestPropertySourceUtils.convertInlinedPropertiesToMap( + configuration.getPropertySourceProperties())); + Binder binder = new Binder(source); + return binder + .bind("spring.main.web-application-type", + Bindable.of(WebApplicationType.class)) + .orElseGet(this::deduceWebApplicationType); } - private WebApplicationType deduceWebApplication() { + private WebApplicationType deduceWebApplicationType() { if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null) && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) { return WebApplicationType.REACTIVE; @@ -228,25 +229,6 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr return false; } - private WebApplicationType getConfiguredWebApplicationType( - MergedContextConfiguration configuration) { - PropertySources sources = convertToPropertySources( - configuration.getPropertySourceProperties()); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - new PropertySourcesPropertyResolver(sources), "spring.main."); - String property = resolver.getProperty("web-application-type"); - return (property != null ? WebApplicationType.valueOf(property.toUpperCase()) - : null); - } - - private PropertySources convertToPropertySources(String[] properties) { - Map source = TestPropertySourceUtils - .convertInlinedPropertiesToMap(properties); - MutablePropertySources sources = new MutablePropertySources(); - sources.addFirst(new MapPropertySource("inline", source)); - return sources; - } - protected Class[] getOrFindConfigurationClasses( MergedContextConfiguration mergedConfig) { Class[] classes = mergedConfig.getClasses(); diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/LocalHostUriTemplateHandler.java b/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/LocalHostUriTemplateHandler.java index 1658597457..f54e67a40b 100644 --- a/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/LocalHostUriTemplateHandler.java +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/LocalHostUriTemplateHandler.java @@ -16,7 +16,6 @@ package org.springframework.boot.test.web.client; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.web.client.RootUriTemplateHandler; import org.springframework.core.env.Environment; import org.springframework.util.Assert; @@ -30,6 +29,7 @@ import org.springframework.web.util.UriTemplateHandler; * @author Phillip Webb * @author Andy Wilkinson * @author Eddú Meléndez + * @author Madhura Bhave * @since 1.4.0 */ public class LocalHostUriTemplateHandler extends RootUriTemplateHandler { @@ -38,7 +38,7 @@ public class LocalHostUriTemplateHandler extends RootUriTemplateHandler { private final String scheme; - private RelaxedPropertyResolver servletPropertyResolver; + private final String prefix = "server.servlet."; /** * Create a new {@code LocalHostUriTemplateHandler} that will generate {@code http} @@ -63,14 +63,13 @@ public class LocalHostUriTemplateHandler extends RootUriTemplateHandler { Assert.notNull(scheme, "Scheme must not be null"); this.environment = environment; this.scheme = scheme; - this.servletPropertyResolver = new RelaxedPropertyResolver(environment, - "server.servlet."); } @Override public String getRootUri() { String port = this.environment.getProperty("local.server.port", "8080"); - String contextPath = this.servletPropertyResolver.getProperty("context-path", ""); + String contextPath = this.environment.getProperty(this.prefix + "context-path", + ""); return this.scheme + "://localhost:" + port + contextPath; } From 69ab2e462e7eaf0158cd3f96644c8fb46c5f5e9a Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 25 Apr 2017 10:52:25 -0700 Subject: [PATCH 09/19] Use new configuration properties in autoconfigure Update `spring-boot-autoconfigure` to use the new configuration properties support. See gh-9000 --- .../AutoConfigurationImportSelector.java | 31 ++------- .../autoconfigure/cache/CacheCondition.java | 31 +++++---- .../cache/EhCacheCacheConfiguration.java | 5 +- .../cache/JCacheCacheConfiguration.java | 7 +- .../condition/ResourceCondition.java | 23 +++---- .../couchbase/OnBootstrapHostsCondition.java | 66 +++++-------------- ...ExceptionTranslationAutoConfiguration.java | 13 ++-- .../CassandraDataAutoConfiguration.java | 16 ++--- .../hazelcast/HazelcastAutoConfiguration.java | 5 +- .../HazelcastConfigResourceCondition.java | 8 +-- .../info/ProjectInfoAutoConfiguration.java | 23 +++---- .../IntegrationAutoConfiguration.java | 17 +++-- .../autoconfigure/jdbc/DataSourceBuilder.java | 19 ++++-- .../jdbc/XADataSourceAutoConfiguration.java | 34 +++++++--- .../jmx/JmxAutoConfiguration.java | 18 ++--- .../MustacheEnvironmentCollector.java | 19 +----- .../MustacheTemplateAvailabilityProvider.java | 11 ++-- .../autoconfigure/orm/jpa/JpaProperties.java | 5 +- .../OAuth2RestOperationsConfiguration.java | 8 +-- .../OAuth2ResourceServerConfiguration.java | 33 ++++++---- ...ourceServerTokenServicesConfiguration.java | 28 ++++---- .../session/SessionCondition.java | 29 ++++---- ...PathBasedTemplateAvailabilityProvider.java | 24 +++---- .../TemplateAvailabilityProviders.java | 9 ++- ...ThymeleafTemplateAvailabilityProvider.java | 9 +-- .../ValidationAutoConfiguration.java | 13 ++-- .../web/OnEnabledResourceChainCondition.java | 8 +-- .../JspTemplateAvailabilityProvider.java | 9 +-- .../cache/CacheAutoConfigurationTests.java | 3 +- .../condition/ResourceConditionTests.java | 6 +- ...tionTranslationAutoConfigurationTests.java | 2 +- .../h2/H2ConsoleAutoConfigurationTests.java | 2 +- .../IntegrationAutoConfigurationTests.java | 8 ++- .../XADataSourceAutoConfigurationTests.java | 7 +- .../MustacheStandaloneIntegrationTests.java | 2 +- .../AbstractJpaAutoConfigurationTests.java | 2 + .../oauth2/OAuth2AutoConfigurationTests.java | 4 ++ ...ServerTokenServicesConfigurationTests.java | 3 +- .../SendGridAutoConfigurationTests.java | 4 +- .../SessionAutoConfigurationTests.java | 5 +- .../FacebookAutoConfigurationTests.java | 4 +- .../LinkedInAutoConfigurationTests.java | 4 +- .../MultiApiAutoConfigurationTests.java | 4 +- .../social/TwitterAutoConfigurationTests.java | 4 +- ...spatcherServletAutoConfigurationTests.java | 2 +- .../servlet/WebMvcAutoConfigurationTests.java | 2 +- .../WebServicesAutoConfigurationTests.java | 2 +- 47 files changed, 276 insertions(+), 315 deletions(-) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java index 4b5ebe85d1..6013b5de06 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java @@ -20,10 +20,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -37,7 +35,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.DeferredImportSelector; @@ -52,7 +50,6 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; /** * {@link DeferredImportSelector} to handle {@link EnableAutoConfiguration @@ -211,28 +208,14 @@ public class AutoConfigurationImportSelector } private List getExcludeAutoConfigurationsProperty() { + String name = "spring.autoconfigure.exclude"; if (getEnvironment() instanceof ConfigurableEnvironment) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - this.environment, "spring.autoconfigure."); - Map properties = resolver.getSubProperties("exclude"); - if (properties.isEmpty()) { - return Collections.emptyList(); - } - List excludes = new ArrayList<>(); - for (Map.Entry entry : properties.entrySet()) { - String name = entry.getKey(); - Object value = entry.getValue(); - if (name.isEmpty() || name.startsWith("[") && value != null) { - excludes.addAll(new HashSet<>(Arrays.asList(StringUtils - .tokenizeToStringArray(String.valueOf(value), ",")))); - } - } - return excludes; + Binder binder = Binder.get(getEnvironment()); + return binder.bind(name, String[].class).map(Arrays::asList) + .orElse(Collections.emptyList()); } - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(getEnvironment(), - "spring.autoconfigure."); - String[] exclude = resolver.getProperty("exclude", String[].class); - return (Arrays.asList(exclude == null ? new String[0] : exclude)); + String[] excludes = getEnvironment().getProperty(name, String[].class); + return (excludes == null ? Collections.emptyList() : Arrays.asList(excludes)); } private List sort(List configurations, diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheCondition.java index a114ce1e14..2fcc93b394 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheCondition.java @@ -19,8 +19,11 @@ package org.springframework.boot.autoconfigure.cache; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.ClassMetadata; @@ -30,6 +33,7 @@ import org.springframework.core.type.ClassMetadata; * * @author Stephane Nicoll * @author Phillip Webb + * @author Madhura Bhave * @since 1.3.0 */ class CacheCondition extends SpringBootCondition { @@ -43,18 +47,23 @@ class CacheCondition extends SpringBootCondition { } ConditionMessage.Builder message = ConditionMessage.forCondition("Cache", sourceClass); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), "spring.cache."); - if (!resolver.containsProperty("type")) { - return ConditionOutcome.match(message.because("automatic cache type")); + Environment environment = context.getEnvironment(); + try { + BindResult specified = Binder.get(environment) + .bind("spring.cache.type", CacheType.class); + if (!specified.isBound()) { + return ConditionOutcome.match(message.because("automatic cache type")); + } + CacheType required = CacheConfigurations + .getType(((AnnotationMetadata) metadata).getClassName()); + if (specified.get() == required) { + return ConditionOutcome + .match(message.because(specified.get() + " cache type")); + } } - CacheType cacheType = CacheConfigurations - .getType(((AnnotationMetadata) metadata).getClassName()); - String value = resolver.getProperty("type").replace('-', '_').toUpperCase(); - if (value.equals(cacheType.name())) { - return ConditionOutcome.match(message.because(value + " cache type")); + catch (BindException ex) { } - return ConditionOutcome.noMatch(message.because(value + " cache type")); + return ConditionOutcome.noMatch(message.because("unknown cache type")); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/EhCacheCacheConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/EhCacheCacheConfiguration.java index f127750329..0c287c9353 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/EhCacheCacheConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/EhCacheCacheConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -35,6 +35,7 @@ import org.springframework.core.io.Resource; * * @author Eddú Meléndez * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.3.0 */ @Configuration @@ -78,7 +79,7 @@ class EhCacheCacheConfiguration { static class ConfigAvailableCondition extends ResourceCondition { ConfigAvailableCondition() { - super("EhCache", "spring.cache.ehcache", "config", "classpath:/ehcache.xml"); + super("EhCache", "spring.cache.ehcache.config", "classpath:/ehcache.xml"); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheCacheConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheCacheConfiguration.java index 4e76716d82..cbdf540f2c 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheCacheConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheCacheConfiguration.java @@ -34,7 +34,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.cache.jcache.JCacheCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; @@ -53,6 +52,7 @@ import org.springframework.util.StringUtils; * Cache configuration for JSR-107 compliant providers. * * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.3.0 */ @Configuration @@ -187,9 +187,8 @@ class JCacheCacheConfiguration { public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { ConditionMessage.Builder message = ConditionMessage.forCondition("JCache"); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), "spring.cache.jcache."); - if (resolver.containsProperty("provider")) { + String providerProperty = "spring.cache.jcache.provider"; + if (context.getEnvironment().containsProperty(providerProperty)) { return ConditionOutcome .match(message.because("JCache provider specified")); } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ResourceCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ResourceCondition.java index ee8c5324c5..5332382b5f 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ResourceCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ResourceCondition.java @@ -22,7 +22,6 @@ import java.util.List; import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder; import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.io.Resource; import org.springframework.core.type.AnnotatedTypeMetadata; @@ -33,42 +32,38 @@ import org.springframework.core.type.AnnotatedTypeMetadata; * * @author Stephane Nicoll * @author Phillip Webb + * @author Madhura Bhave * @since 1.3.0 */ public abstract class ResourceCondition extends SpringBootCondition { private final String name; - private final String prefix; - - private final String propertyName; + private final String property; private final String[] resourceLocations; /** * Create a new condition. * @param name the name of the component - * @param prefix the prefix of the configuration key - * @param propertyName the name of the configuration key + * @param property the configuration property * @param resourceLocations default location(s) where the configuration file can be * found if the configuration key is not specified + * @since 2.0.0 */ - protected ResourceCondition(String name, String prefix, String propertyName, + protected ResourceCondition(String name, String property, String... resourceLocations) { this.name = name; - this.prefix = (prefix.endsWith(".") ? prefix : prefix + "."); - this.propertyName = propertyName; + this.property = property; this.resourceLocations = resourceLocations; } @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), this.prefix); - if (resolver.containsProperty(this.propertyName)) { - return ConditionOutcome.match(startConditionMessage() - .foundExactly("property " + this.prefix + this.propertyName)); + if (context.getEnvironment().containsProperty(this.property)) { + return ConditionOutcome.match( + startConditionMessage().foundExactly("property " + this.property)); } return getResourceOutcome(context, metadata); } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/OnBootstrapHostsCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/OnBootstrapHostsCondition.java index 0fcc190a37..2473ec6aa5 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/OnBootstrapHostsCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/OnBootstrapHostsCondition.java @@ -16,76 +16,42 @@ package org.springframework.boot.autoconfigure.couchbase; -import java.util.AbstractMap; -import java.util.HashMap; -import java.util.Map; +import java.util.List; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.PropertySourcesPropertyValues; -import org.springframework.boot.bind.RelaxedDataBinder; -import org.springframework.boot.bind.RelaxedNames; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.ConditionContext; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.core.env.PropertySources; import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.validation.DataBinder; /** * Condition to determine if {@code spring.couchbase.bootstrap-hosts} is specified. * * @author Stephane Nicoll + * @author Madhura Bhave */ class OnBootstrapHostsCondition extends SpringBootCondition { + private static final Bindable> STRING_LIST = Bindable + .listOf(String.class); + @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - Environment environment = context.getEnvironment(); - PropertyResolver resolver = new PropertyResolver( - ((ConfigurableEnvironment) environment).getPropertySources(), - "spring.couchbase"); - Map.Entry entry = resolver.resolveProperty("bootstrap-hosts"); - if (entry != null) { + String name = "spring.couchbase.bootstrap-hosts"; + BindResult property = Binder.get(context.getEnvironment()).bind(name, + STRING_LIST); + if (property.isBound()) { return ConditionOutcome.match(ConditionMessage .forCondition(OnBootstrapHostsCondition.class.getName()) - .found("property").items("spring.couchbase.bootstrap-hosts")); + .found("property").items(name)); } - return ConditionOutcome.noMatch(ConditionMessage - .forCondition(OnBootstrapHostsCondition.class.getName()) - .didNotFind("property").items("spring.couchbase.bootstrap-hosts")); - } - - private static class PropertyResolver { - - private final String prefix; - - private final Map content; - - PropertyResolver(PropertySources propertySources, String prefix) { - this.prefix = prefix; - this.content = new HashMap<>(); - DataBinder binder = new RelaxedDataBinder(this.content, this.prefix); - binder.bind(new PropertySourcesPropertyValues(propertySources)); - } - - Map.Entry resolveProperty(String name) { - RelaxedNames prefixes = new RelaxedNames(this.prefix); - RelaxedNames keys = new RelaxedNames(name); - for (String prefix : prefixes) { - for (String relaxedKey : keys) { - String key = prefix + relaxedKey; - if (this.content.containsKey(relaxedKey)) { - return new AbstractMap.SimpleEntry<>(key, - this.content.get(relaxedKey)); - } - } - } - return null; - } - + return ConditionOutcome.noMatch( + ConditionMessage.forCondition(OnBootstrapHostsCondition.class.getName()) + .didNotFind("property").items(name)); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfiguration.java index 32084cb0da..3810e8a6f7 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfiguration.java @@ -20,7 +20,6 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; @@ -31,6 +30,7 @@ import org.springframework.dao.annotation.PersistenceExceptionTranslationPostPro * * @author Andy Wilkinson * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.2.0 */ @ConditionalOnClass(PersistenceExceptionTranslationPostProcessor.class) @@ -42,15 +42,10 @@ public class PersistenceExceptionTranslationAutoConfiguration { public static PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor( Environment environment) { PersistenceExceptionTranslationPostProcessor postProcessor = new PersistenceExceptionTranslationPostProcessor(); - postProcessor.setProxyTargetClass(determineProxyTargetClass(environment)); + boolean proxyTargetClass = environment.getProperty( + "spring.aop.proxy-target-class", Boolean.class, Boolean.TRUE); + postProcessor.setProxyTargetClass(proxyTargetClass); return postProcessor; } - private static boolean determineProxyTargetClass(Environment environment) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, - "spring.aop."); - Boolean value = resolver.getProperty("proxyTargetClass", Boolean.class); - return (value != null ? value : true); - } - } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java index 55d515ab97..c37fae747d 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java @@ -30,12 +30,11 @@ import org.springframework.boot.autoconfigure.cassandra.CassandraProperties; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.domain.EntityScanPackages; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import org.springframework.core.env.PropertyResolver; import org.springframework.data.cassandra.config.CassandraEntityClassScanner; import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; import org.springframework.data.cassandra.config.SchemaAction; @@ -54,6 +53,7 @@ import org.springframework.util.StringUtils; * @author Julien Dubois * @author Eddú Meléndez * @author Mark Paluch + * @author Madhura Bhave * @since 1.3.0 */ @Configuration @@ -68,15 +68,14 @@ public class CassandraDataAutoConfiguration { private final Cluster cluster; - private final PropertyResolver propertyResolver; + private final Environment environment; public CassandraDataAutoConfiguration(BeanFactory beanFactory, CassandraProperties properties, Cluster cluster, Environment environment) { this.beanFactory = beanFactory; this.properties = properties; this.cluster = cluster; - this.propertyResolver = new RelaxedPropertyResolver(environment, - "spring.data.cassandra."); + this.environment = environment; } @Bean @@ -112,10 +111,9 @@ public class CassandraDataAutoConfiguration { session.setCluster(this.cluster); session.setConverter(converter); session.setKeyspaceName(this.properties.getKeyspaceName()); - String name = this.propertyResolver.getProperty("schemaAction", - SchemaAction.NONE.name()); - SchemaAction schemaAction = SchemaAction.valueOf(name.toUpperCase()); - session.setSchemaAction(schemaAction); + Binder binder = Binder.get(this.environment); + binder.bind("spring.data.cassandra.schema-action", SchemaAction.class) + .ifBound(session::setSchemaAction); return session; } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfiguration.java index 0f94847239..1eb7d23b81 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -38,6 +38,7 @@ import org.springframework.core.io.Resource; * configuration file is found in the environment. * * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.3.0 * @see HazelcastConfigResourceCondition */ @@ -87,7 +88,7 @@ public class HazelcastAutoConfiguration { static class ConfigAvailableCondition extends HazelcastConfigResourceCondition { ConfigAvailableCondition() { - super("spring.hazelcast", "config"); + super("spring.hazelcast.config"); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastConfigResourceCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastConfigResourceCondition.java index 0dd8cc6961..0bec54d50a 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastConfigResourceCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastConfigResourceCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -28,15 +28,15 @@ import org.springframework.core.type.AnnotatedTypeMetadata; * property referring to the resource to use has been set. * * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.3.0 */ public abstract class HazelcastConfigResourceCondition extends ResourceCondition { static final String CONFIG_SYSTEM_PROPERTY = "hazelcast.config"; - protected HazelcastConfigResourceCondition(String prefix, String propertyName) { - super("Hazelcast", prefix, propertyName, "file:./hazelcast.xml", - "classpath:/hazelcast.xml"); + protected HazelcastConfigResourceCondition(String property) { + super("Hazelcast", property, "file:./hazelcast.xml", "classpath:/hazelcast.xml"); } @Override diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java index 26690bb492..17f9c28f3e 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -25,7 +25,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.info.BuildProperties; import org.springframework.boot.info.GitProperties; @@ -33,7 +32,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.Environment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -44,6 +43,7 @@ import org.springframework.core.type.AnnotatedTypeMetadata; * {@link EnableAutoConfiguration Auto-configuration} for various project information. * * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.4.0 */ @Configuration @@ -91,19 +91,12 @@ public class ProjectInfoAutoConfiguration { public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { ResourceLoader loader = context.getResourceLoader(); - if (loader == null) { - loader = this.defaultResourceLoader; - } - PropertyResolver propertyResolver = context.getEnvironment(); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - propertyResolver, "spring.info.git."); - String location = resolver.getProperty("location"); + loader = (loader != null ? loader : this.defaultResourceLoader); + Environment environment = context.getEnvironment(); + String location = environment.getProperty("spring.info.git.location"); if (location == null) { - resolver = new RelaxedPropertyResolver(propertyResolver, "spring.git."); - location = resolver.getProperty("properties"); - if (location == null) { - location = "classpath:git.properties"; - } + location = environment.getProperty("spring.git.properties"); + location = (location != null ? location : "classpath:git.properties"); } ConditionMessage.Builder message = ConditionMessage .forCondition("GitResource"); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java index bdf301d8c2..21640d89df 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java @@ -29,7 +29,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.Bean; @@ -54,6 +53,7 @@ import org.springframework.util.StringUtils; * @author Dave Syer * @author Stephane Nicoll * @author Vedran Pavic + * @author Madhura Bhave * @since 1.1.0 */ @Configuration @@ -83,7 +83,7 @@ public class IntegrationAutoConfiguration { private BeanFactory beanFactory; - private RelaxedPropertyResolver propertyResolver; + private Environment environment; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { @@ -92,21 +92,20 @@ public class IntegrationAutoConfiguration { @Override public void setEnvironment(Environment environment) { - this.propertyResolver = new RelaxedPropertyResolver(environment, - "spring.jmx."); + this.environment = environment; } @Bean public IntegrationMBeanExporter integrationMbeanExporter() { IntegrationMBeanExporter exporter = new IntegrationMBeanExporter(); - String defaultDomain = this.propertyResolver.getProperty("default-domain"); + String defaultDomain = this.environment + .getProperty("spring.jmx.default-domain"); if (StringUtils.hasLength(defaultDomain)) { exporter.setDefaultDomain(defaultDomain); } - String server = this.propertyResolver.getProperty("server", "mbeanServer"); - if (StringUtils.hasLength(server)) { - exporter.setServer(this.beanFactory.getBean(server, MBeanServer.class)); - } + String serverBean = this.environment.getProperty("spring.jmx.server", + "mbeanServer"); + exporter.setServer(this.beanFactory.getBean(serverBean, MBeanServer.class)); return exporter; } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceBuilder.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceBuilder.java index 8419d95c03..930d4bc769 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceBuilder.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceBuilder.java @@ -22,8 +22,12 @@ import java.util.Map; import javax.sql.DataSource; import org.springframework.beans.BeanUtils; -import org.springframework.beans.MutablePropertyValues; -import org.springframework.boot.bind.RelaxedDataBinder; +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.ConfigurationPropertyNameAliases; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.util.ClassUtils; @@ -37,6 +41,7 @@ import org.springframework.util.ClassUtils; * {@code @ConfigurationProperties}. * * @author Dave Syer + * @author Madhura Bhave * @since 1.1.0 */ public class DataSourceBuilder { @@ -82,9 +87,13 @@ public class DataSourceBuilder { } private void bind(DataSource result) { - MutablePropertyValues properties = new MutablePropertyValues(this.properties); - new RelaxedDataBinder(result).withAlias("url", "jdbcUrl") - .withAlias("username", "user").bind(properties); + ConfigurationPropertySource source = new MapConfigurationPropertySource( + this.properties); + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("url", "jdbc-url"); + aliases.addAlaises("username", "user"); + Binder binder = new Binder(source.withAliases(aliases)); + binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result)); } public DataSourceBuilder type(Class type) { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java index 810b440a42..11c9721c7a 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -21,7 +21,6 @@ import javax.sql.XADataSource; import javax.transaction.TransactionManager; import org.springframework.beans.BeanUtils; -import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -29,8 +28,13 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.bind.RelaxedDataBinder; import org.springframework.boot.context.properties.EnableConfigurationProperties; +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.ConfigurationPropertyNameAliases; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.jta.XADataSourceWrapper; import org.springframework.context.annotation.Bean; @@ -44,6 +48,7 @@ import org.springframework.util.StringUtils; * * @author Phillip Webb * @author Josh Long + * @author Madhura Bhave * @since 1.2.0 */ @AutoConfigureBefore(DataSourceAutoConfiguration.class) @@ -105,13 +110,22 @@ public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { } } - private void bindXaProperties(XADataSource target, DataSourceProperties properties) { - MutablePropertyValues values = new MutablePropertyValues(); - values.add("user", this.properties.determineUsername()); - values.add("password", this.properties.determinePassword()); - values.add("url", this.properties.determineUrl()); - values.addPropertyValues(properties.getXa().getProperties()); - new RelaxedDataBinder(target).withAlias("user", "username").bind(values); + private void bindXaProperties(XADataSource target, + DataSourceProperties dataSourceProperties) { + Binder binder = new Binder(getBinderSource(dataSourceProperties)); + binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(target)); + } + + private ConfigurationPropertySource getBinderSource( + DataSourceProperties dataSourceProperties) { + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.put("user", this.properties.determineUsername()); + source.put("password", this.properties.determinePassword()); + source.put("url", this.properties.determineUrl()); + source.putAll(dataSourceProperties.getXa().getProperties()); + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("user", "username"); + return source.withAliases(aliases); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/JmxAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/JmxAutoConfiguration.java index ab04327001..9b7f18e9e0 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/JmxAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/JmxAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * 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. @@ -26,7 +26,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.SearchStrategy; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -49,19 +48,20 @@ import org.springframework.util.StringUtils; * To disable auto export of annotation beans set {@code spring.jmx.enabled: false}. * * @author Christian Dupuis + * @author Madhura Bhave */ @Configuration @ConditionalOnClass({ MBeanExporter.class }) @ConditionalOnProperty(prefix = "spring.jmx", name = "enabled", havingValue = "true", matchIfMissing = true) public class JmxAutoConfiguration implements EnvironmentAware, BeanFactoryAware { - private RelaxedPropertyResolver propertyResolver; + private Environment environment; private BeanFactory beanFactory; @Override public void setEnvironment(Environment environment) { - this.propertyResolver = new RelaxedPropertyResolver(environment, "spring.jmx."); + this.environment = environment; } @Override @@ -76,9 +76,10 @@ public class JmxAutoConfiguration implements EnvironmentAware, BeanFactoryAware AnnotationMBeanExporter exporter = new AnnotationMBeanExporter(); exporter.setRegistrationPolicy(RegistrationPolicy.FAIL_ON_EXISTING); exporter.setNamingStrategy(namingStrategy); - String server = this.propertyResolver.getProperty("server", "mbeanServer"); - if (StringUtils.hasLength(server)) { - exporter.setServer(this.beanFactory.getBean(server, MBeanServer.class)); + String serverBean = this.environment.getProperty("spring.jmx.server", + "mbeanServer"); + if (StringUtils.hasLength(serverBean)) { + exporter.setServer(this.beanFactory.getBean(serverBean, MBeanServer.class)); } return exporter; } @@ -88,7 +89,7 @@ public class JmxAutoConfiguration implements EnvironmentAware, BeanFactoryAware public ParentAwareNamingStrategy objectNamingStrategy() { ParentAwareNamingStrategy namingStrategy = new ParentAwareNamingStrategy( new AnnotationJmxAttributeSource()); - String defaultDomain = this.propertyResolver.getProperty("default-domain"); + String defaultDomain = this.environment.getProperty("spring.jmx.default-domain"); if (StringUtils.hasLength(defaultDomain)) { namingStrategy.setDefaultDomain(defaultDomain); } @@ -106,7 +107,6 @@ public class JmxAutoConfiguration implements EnvironmentAware, BeanFactoryAware factory.setLocateExistingServerIfPossible(true); factory.afterPropertiesSet(); return factory.getObject(); - } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheEnvironmentCollector.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheEnvironmentCollector.java index e99691dcca..92c4170d47 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheEnvironmentCollector.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheEnvironmentCollector.java @@ -16,16 +16,10 @@ package org.springframework.boot.autoconfigure.mustache; -import java.util.HashMap; -import java.util.Map; - import com.samskivert.mustache.DefaultCollector; import com.samskivert.mustache.Mustache.Collector; import com.samskivert.mustache.Mustache.VariableFetcher; -import org.springframework.boot.bind.PropertySourcesPropertyValues; -import org.springframework.boot.bind.RelaxedDataBinder; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; @@ -34,6 +28,7 @@ import org.springframework.core.env.Environment; * Mustache {@link Collector} to expose properties from the Spring {@link Environment}. * * @author Dave Syer + * @author Madhura Bhave * @since 1.2.2 */ public class MustacheEnvironmentCollector extends DefaultCollector @@ -41,19 +36,11 @@ public class MustacheEnvironmentCollector extends DefaultCollector private ConfigurableEnvironment environment; - private Map target; - - private RelaxedPropertyResolver propertyResolver; - private final VariableFetcher propertyFetcher = new PropertyVariableFetcher(); @Override public void setEnvironment(Environment environment) { this.environment = (ConfigurableEnvironment) environment; - this.target = new HashMap<>(); - new RelaxedDataBinder(this.target).bind( - new PropertySourcesPropertyValues(this.environment.getPropertySources())); - this.propertyResolver = new RelaxedPropertyResolver(environment); } @Override @@ -62,7 +49,7 @@ public class MustacheEnvironmentCollector extends DefaultCollector if (fetcher != null) { return fetcher; } - if (this.propertyResolver.containsProperty(name)) { + if (this.environment.containsProperty(name)) { return this.propertyFetcher; } return null; @@ -72,7 +59,7 @@ public class MustacheEnvironmentCollector extends DefaultCollector @Override public Object get(Object ctx, String name) throws Exception { - return MustacheEnvironmentCollector.this.propertyResolver.getProperty(name); + return MustacheEnvironmentCollector.this.environment.getProperty(name); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheTemplateAvailabilityProvider.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheTemplateAvailabilityProvider.java index 2454878863..0019f1020f 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheTemplateAvailabilityProvider.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheTemplateAvailabilityProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * 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. @@ -17,9 +17,7 @@ package org.springframework.boot.autoconfigure.mustache; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.core.env.Environment; -import org.springframework.core.env.PropertyResolver; import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; @@ -28,6 +26,7 @@ import org.springframework.util.ClassUtils; * Mustache view templates. * * @author Dave Syer + * @author Madhura Bhave * @since 1.2.2 */ public class MustacheTemplateAvailabilityProvider @@ -37,11 +36,9 @@ public class MustacheTemplateAvailabilityProvider public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader, ResourceLoader resourceLoader) { if (ClassUtils.isPresent("com.samskivert.mustache.Template", classLoader)) { - PropertyResolver resolver = new RelaxedPropertyResolver(environment, - "spring.mustache."); - String prefix = resolver.getProperty("prefix", + String prefix = environment.getProperty("spring.mustache.prefix", MustacheProperties.DEFAULT_PREFIX); - String suffix = resolver.getProperty("suffix", + String suffix = environment.getProperty("spring.mustache.suffix", MustacheProperties.DEFAULT_SUFFIX); return resourceLoader.getResource(prefix + view + suffix).exists(); } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java index eb253ce916..ea105d9e10 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java @@ -34,6 +34,7 @@ import org.springframework.util.StringUtils; * @author Andy Wilkinson * @author Stephane Nicoll * @author Eddú Meléndez + * @author Madhura Bhave * @since 1.1.0 */ @ConfigurationProperties(prefix = "spring.jpa") @@ -169,11 +170,11 @@ public class JpaProperties { this.ddlAuto = ddlAuto; } - public boolean isUseNewIdGeneratorMappings() { + public Boolean isUseNewIdGeneratorMappings() { return this.useNewIdGeneratorMappings; } - public void setUseNewIdGeneratorMappings(boolean useNewIdGeneratorMappings) { + public void setUseNewIdGeneratorMappings(Boolean useNewIdGeneratorMappings) { this.useNewIdGeneratorMappings = useNewIdGeneratorMappings; } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2RestOperationsConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2RestOperationsConfiguration.java index 929a6af72f..48be1385d7 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2RestOperationsConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2RestOperationsConfiguration.java @@ -31,7 +31,6 @@ import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2RestOperationsConfiguration.OAuth2ClientIdCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; @@ -42,7 +41,6 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.core.env.PropertyResolver; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -62,6 +60,7 @@ import org.springframework.util.StringUtils; * Configuration for OAuth2 Single Sign On REST operations. * * @author Dave Syer + * @author Madhura Bhave * @since 1.3.0 */ @Configuration @@ -159,9 +158,8 @@ public class OAuth2RestOperationsConfiguration { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - PropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), "security.oauth2.client."); - String clientId = resolver.getProperty("client-id"); + String clientId = context.getEnvironment() + .getProperty("security.oauth2.client.client-id"); ConditionMessage.Builder message = ConditionMessage .forCondition("OAuth Client ID"); if (StringUtils.hasLength(clientId)) { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerConfiguration.java index a1d2a6e84e..58c68874df 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource; +import java.util.Map; + import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; @@ -28,7 +30,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerConfiguration.ResourceServerCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; @@ -39,6 +42,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ConfigurationCondition; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.StandardAnnotationMetadata; @@ -58,6 +62,7 @@ import org.springframework.util.StringUtils; * * @author Greg Turnquist * @author Dave Syer + * @author Madhura Bhave * @since 1.3.0 */ @Configuration @@ -150,6 +155,9 @@ public class OAuth2ResourceServerConfiguration { protected static class ResourceServerCondition extends SpringBootCondition implements ConfigurationCondition { + private static final Bindable> STRING_OBJECT_MAP = Bindable + .mapOf(String.class, Object.class); + private static final String AUTHORIZATION_ANNOTATION = "org.springframework." + "security.oauth2.config.annotation.web.configuration." + "AuthorizationServerEndpointsConfiguration"; @@ -165,24 +173,28 @@ public class OAuth2ResourceServerConfiguration { ConditionMessage.Builder message = ConditionMessage .forCondition("OAuth ResourceServer Condition"); Environment environment = context.getEnvironment(); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, - "security.oauth2.resource."); + if (!(environment instanceof ConfigurableEnvironment)) { + return ConditionOutcome + .noMatch(message.didNotFind("A ConfigurableEnvironment").atAll()); + } if (hasOAuthClientId(environment)) { return ConditionOutcome.match(message.foundExactly("client-id property")); } - if (!resolver.getSubProperties("jwt").isEmpty()) { + Binder binder = Binder.get(environment); + String prefix = "security.oauth2.resource."; + if (binder.bind(prefix + "jwt", STRING_OBJECT_MAP).isBound()) { return ConditionOutcome .match(message.foundExactly("JWT resource configuration")); } - if (!resolver.getSubProperties("jwk").isEmpty()) { + if (binder.bind(prefix + "jwk", STRING_OBJECT_MAP).isBound()) { return ConditionOutcome .match(message.foundExactly("JWK resource configuration")); } - if (StringUtils.hasText(resolver.getProperty("user-info-uri"))) { + if (StringUtils.hasText(environment.getProperty(prefix + "user-info-uri"))) { return ConditionOutcome .match(message.foundExactly("user-info-uri property")); } - if (StringUtils.hasText(resolver.getProperty("token-info-uri"))) { + if (StringUtils.hasText(environment.getProperty(prefix + "token-info-uri"))) { return ConditionOutcome .match(message.foundExactly("token-info-uri property")); } @@ -194,14 +206,13 @@ public class OAuth2ResourceServerConfiguration { } } return ConditionOutcome.noMatch( - message.didNotFind("client id, JWT resource or authorization server") + message.didNotFind("client ID, JWT resource or authorization server") .atAll()); } private boolean hasOAuthClientId(Environment environment) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, - "security.oauth2.client."); - return StringUtils.hasLength(resolver.getProperty("client-id", "")); + return StringUtils.hasLength( + environment.getProperty("security.oauth2.client.client-id")); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java index 056a8b7775..112237ac47 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java @@ -30,7 +30,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; @@ -319,17 +318,17 @@ public class ResourceServerTokenServicesConfiguration { ConditionMessage.Builder message = ConditionMessage .forCondition("OAuth TokenInfo Condition"); Environment environment = context.getEnvironment(); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, - "security.oauth2.resource."); - Boolean preferTokenInfo = resolver.getProperty("prefer-token-info", - Boolean.class); + Boolean preferTokenInfo = environment.getProperty( + "security.oauth2.resource.prefer-token-info", Boolean.class); if (preferTokenInfo == null) { preferTokenInfo = environment .resolvePlaceholders("${OAUTH2_RESOURCE_PREFERTOKENINFO:true}") .equals("true"); } - String tokenInfoUri = resolver.getProperty("token-info-uri"); - String userInfoUri = resolver.getProperty("user-info-uri"); + String tokenInfoUri = environment + .getProperty("security.oauth2.resource.token-info-uri"); + String userInfoUri = environment + .getProperty("security.oauth2.resource.user-info-uri"); if (!StringUtils.hasLength(userInfoUri) && !StringUtils.hasLength(tokenInfoUri)) { return ConditionOutcome @@ -351,10 +350,11 @@ public class ResourceServerTokenServicesConfiguration { AnnotatedTypeMetadata metadata) { ConditionMessage.Builder message = ConditionMessage .forCondition("OAuth JWT Condition"); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), "security.oauth2.resource.jwt."); - String keyValue = resolver.getProperty("key-value"); - String keyUri = resolver.getProperty("key-uri"); + Environment environment = context.getEnvironment(); + String keyValue = environment + .getProperty("security.oauth2.resource.jwt.key-value"); + String keyUri = environment + .getProperty("security.oauth2.resource.jwt.key-uri"); if (StringUtils.hasText(keyValue) || StringUtils.hasText(keyUri)) { return ConditionOutcome .match(message.foundExactly("provided public key")); @@ -372,9 +372,9 @@ public class ResourceServerTokenServicesConfiguration { AnnotatedTypeMetadata metadata) { ConditionMessage.Builder message = ConditionMessage .forCondition("OAuth JWK Condition"); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), "security.oauth2.resource.jwk."); - String keyUri = resolver.getProperty("key-set-uri"); + Environment environment = context.getEnvironment(); + String keyUri = environment + .getProperty("security.oauth2.resource.jwk.key-set-uri"); if (StringUtils.hasText(keyUri)) { return ConditionOutcome .match(message.foundExactly("provided jwk key set URI")); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionCondition.java index ea2f4cd7aa..5c142f104f 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionCondition.java @@ -19,8 +19,10 @@ package org.springframework.boot.autoconfigure.session; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.AnnotationMetadata; @@ -29,6 +31,7 @@ import org.springframework.core.type.AnnotationMetadata; * * @author Tommy Ludwig * @author Stephane Nicoll + * @author Madhura Bhave */ class SessionCondition extends SpringBootCondition { @@ -37,21 +40,25 @@ class SessionCondition extends SpringBootCondition { AnnotatedTypeMetadata metadata) { ConditionMessage.Builder message = ConditionMessage .forCondition("Session Condition"); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), "spring.session."); - StoreType sessionStoreType = SessionStoreMappings + Environment environment = context.getEnvironment(); + StoreType required = SessionStoreMappings .getType(((AnnotationMetadata) metadata).getClassName()); - if (!resolver.containsProperty("store-type")) { + if (!environment.containsProperty("spring.session.store-type")) { return ConditionOutcome.noMatch( message.didNotFind("spring.session.store-type property").atAll()); } - String value = resolver.getProperty("store-type").replace('-', '_').toUpperCase(); - if (value.equals(sessionStoreType.name())) { - return ConditionOutcome.match(message - .found("spring.session.store-type property").items(sessionStoreType)); + try { + Binder binder = Binder.get(environment); + return binder.bind("spring.session.store-type", StoreType.class) + .map((t) -> new ConditionOutcome(t == required, + message.found("spring.session.store-type property").items(t))) + .orElse(ConditionOutcome.noMatch(message + .didNotFind("spring.session.store-type property").atAll())); + } + catch (BindException ex) { + return ConditionOutcome.noMatch( + message.found("invalid spring.session.store-type property").atAll()); } - return ConditionOutcome.noMatch( - message.found("spring.session.store-type property").items(value)); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/PathBasedTemplateAvailabilityProvider.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/PathBasedTemplateAvailabilityProvider.java index 5079b660ec..9217b4f4ba 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/PathBasedTemplateAvailabilityProvider.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/PathBasedTemplateAvailabilityProvider.java @@ -18,10 +18,8 @@ package org.springframework.boot.autoconfigure.template; import java.util.List; -import org.springframework.beans.BeanUtils; -import org.springframework.boot.bind.PropertySourcesPropertyValues; -import org.springframework.boot.bind.RelaxedDataBinder; -import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.boot.autoconfigure.template.PathBasedTemplateAvailabilityProvider.TemplateAvailabilityProperties; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; @@ -32,20 +30,20 @@ import org.springframework.util.ClassUtils; * * @author Andy Wilkinson * @author Phillip Webb + * @author Madhura Bhave * @since 1.4.6 */ -public abstract class PathBasedTemplateAvailabilityProvider +public abstract class PathBasedTemplateAvailabilityProvider implements TemplateAvailabilityProvider { private final String className; - private final Class propertiesClass; + private final Class propertiesClass; private final String propertyPrefix; public PathBasedTemplateAvailabilityProvider(String className, - Class propertiesClass, - String propertyPrefix) { + Class propertiesClass, String propertyPrefix) { this.className = className; this.propertiesClass = propertiesClass; this.propertyPrefix = propertyPrefix; @@ -55,12 +53,10 @@ public abstract class PathBasedTemplateAvailabilityProvider public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader, ResourceLoader resourceLoader) { if (ClassUtils.isPresent(this.className, classLoader)) { - TemplateAvailabilityProperties properties = BeanUtils - .instantiateClass(this.propertiesClass); - RelaxedDataBinder binder = new RelaxedDataBinder(properties, - this.propertyPrefix); - binder.bind(new PropertySourcesPropertyValues( - ((ConfigurableEnvironment) environment).getPropertySources())); + Binder binder = Binder.get(environment); + TemplateAvailabilityProperties properties = binder + .bind(this.propertyPrefix, this.propertiesClass) + .orElseCreate(this.propertiesClass); return isTemplateAvailable(view, resourceLoader, properties); } return false; diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java index 9b073f5409..3e729ab93f 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.ApplicationContext; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; @@ -36,6 +35,7 @@ import org.springframework.util.Assert; * {@code spring.template.provider.cache} property is set to {@code false}. * * @author Phillip Webb + * @author Madhura Bhave * @since 1.4.0 */ public class TemplateAvailabilityProviders { @@ -134,10 +134,9 @@ public class TemplateAvailabilityProviders { Assert.notNull(environment, "Environment must not be null"); Assert.notNull(classLoader, "ClassLoader must not be null"); Assert.notNull(resourceLoader, "ResourceLoader must not be null"); - - RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver( - environment, "spring.template.provider."); - if (!propertyResolver.getProperty("cache", Boolean.class, true)) { + Boolean useCache = environment.getProperty("spring.template.provider.cache", + Boolean.class, true); + if (!useCache) { return findProvider(view, environment, classLoader, resourceLoader); } TemplateAvailabilityProvider provider = this.resolved.get(view); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafTemplateAvailabilityProvider.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafTemplateAvailabilityProvider.java index b99c3ac9d1..cb0b3eaf55 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafTemplateAvailabilityProvider.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafTemplateAvailabilityProvider.java @@ -17,9 +17,7 @@ package org.springframework.boot.autoconfigure.thymeleaf; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.core.env.Environment; -import org.springframework.core.env.PropertyResolver; import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; @@ -28,6 +26,7 @@ import org.springframework.util.ClassUtils; * Thymeleaf view templates. * * @author Andy Wilkinson + * @author Madhura Bhave * @since 1.1.0 */ public class ThymeleafTemplateAvailabilityProvider @@ -38,11 +37,9 @@ public class ThymeleafTemplateAvailabilityProvider ClassLoader classLoader, ResourceLoader resourceLoader) { if (ClassUtils.isPresent("org.thymeleaf.spring5.SpringTemplateEngine", classLoader)) { - PropertyResolver resolver = new RelaxedPropertyResolver(environment, - "spring.thymeleaf."); - String prefix = resolver.getProperty("prefix", + String prefix = environment.getProperty("spring.thymeleaf.prefix", ThymeleafProperties.DEFAULT_PREFIX); - String suffix = resolver.getProperty("suffix", + String suffix = environment.getProperty("spring.thymeleaf.suffix", ThymeleafProperties.DEFAULT_SUFFIX); return resourceLoader.getResource(prefix + view + suffix).exists(); } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java index 99c4efa6b9..32abaccad1 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java @@ -24,7 +24,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -36,6 +35,7 @@ import org.springframework.validation.beanvalidation.MethodValidationPostProcess * infrastructure. * * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.5.0 */ @Configuration @@ -51,16 +51,11 @@ public class ValidationAutoConfiguration { public static MethodValidationPostProcessor methodValidationPostProcessor( Environment environment, Validator validator) { MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); - processor.setProxyTargetClass(determineProxyTargetClass(environment)); + boolean proxyTargetClass = environment + .getProperty("spring.aop.proxy-target-class", Boolean.class, true); + processor.setProxyTargetClass(proxyTargetClass); processor.setValidator(validator); return processor; } - private static boolean determineProxyTargetClass(Environment environment) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, - "spring.aop."); - Boolean value = resolver.getProperty("proxyTargetClass", Boolean.class); - return (value != null ? value : true); - } - } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java index 52e702c5b6..b209d9d1dc 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java @@ -19,11 +19,9 @@ package org.springframework.boot.autoconfigure.web; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.PropertyResolver; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.ClassUtils; @@ -33,6 +31,7 @@ import org.springframework.util.ClassUtils; * * @author Stephane Nicoll * @author Phillip Webb + * @author Madhura Bhave * @see ConditionalOnEnabledResourceChain */ class OnEnabledResourceChainCondition extends SpringBootCondition { @@ -66,9 +65,8 @@ class OnEnabledResourceChainCondition extends SpringBootCondition { private Boolean getEnabledProperty(ConfigurableEnvironment environment, String key, Boolean defaultValue) { - PropertyResolver resolver = new RelaxedPropertyResolver(environment, - "spring.resources.chain." + key); - return resolver.getProperty("enabled", Boolean.class, defaultValue); + String name = "spring.resources.chain." + key + "enabled"; + return environment.getProperty(name, Boolean.class, defaultValue); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JspTemplateAvailabilityProvider.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JspTemplateAvailabilityProvider.java index b2d7b14386..b5298f99c3 100755 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JspTemplateAvailabilityProvider.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JspTemplateAvailabilityProvider.java @@ -17,9 +17,7 @@ package org.springframework.boot.autoconfigure.web.servlet; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.core.env.Environment; -import org.springframework.core.env.PropertyResolver; import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; @@ -29,6 +27,7 @@ import org.springframework.util.ClassUtils; * * @author Andy Wilkinson * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.1.0 */ public class JspTemplateAvailabilityProvider implements TemplateAvailabilityProvider { @@ -44,11 +43,9 @@ public class JspTemplateAvailabilityProvider implements TemplateAvailabilityProv } private String getResourceName(String view, Environment environment) { - PropertyResolver resolver = new RelaxedPropertyResolver(environment, - "spring.mvc.view."); - String prefix = resolver.getProperty("prefix", + String prefix = environment.getProperty("spring.mvc.view.prefix", WebMvcAutoConfiguration.DEFAULT_PREFIX); - String suffix = resolver.getProperty("suffix", + String suffix = environment.getProperty("spring.mvc.view.suffix", WebMvcAutoConfiguration.DEFAULT_SUFFIX); return prefix + view + suffix; } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java index bfa4179224..65258ec12c 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java @@ -145,8 +145,7 @@ public class CacheAutoConfigurationTests { @Test public void notSupportedCachingMode() { this.thrown.expect(BeanCreationException.class); - this.thrown.expectMessage("cache"); - this.thrown.expectMessage("foobar"); + this.thrown.expectMessage("Failed to bind properties under 'spring.cache.type'"); load(DefaultCacheConfiguration.class, "spring.cache.type=foobar"); } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ResourceConditionTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ResourceConditionTests.java index 7cfd5440df..71f7a26a56 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ResourceConditionTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ResourceConditionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -96,7 +96,7 @@ public class ResourceConditionTests { private static class DefaultLocationResourceCondition extends ResourceCondition { DefaultLocationResourceCondition() { - super("test", "spring.foo.test.", "config", "classpath:/logging.properties"); + super("test", "spring.foo.test.config", "classpath:/logging.properties"); } } @@ -105,7 +105,7 @@ public class ResourceConditionTests { extends ResourceCondition { UnknownDefaultLocationResourceCondition() { - super("test", "spring.foo.test", "config", + super("test", "spring.foo.test.config", "classpath:/this-file-does-not-exist.xml"); } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfigurationTests.java index 495797452b..0e4c1fdc78 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfigurationTests.java @@ -67,7 +67,7 @@ public class PersistenceExceptionTranslationAutoConfigurationTests { public void exceptionTranslationPostProcessorCanBeConfiguredToUseJdkProxy() { this.context = new AnnotationConfigApplicationContext(); EnvironmentTestUtils.addEnvironment(this.context, - "spring.aop.proxyTargetClass=false"); + "spring.aop.proxy-target-class=false"); this.context.register(PersistenceExceptionTranslationAutoConfiguration.class); this.context.refresh(); Map beans = this.context diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java index 02970419a3..86f3bd5c70 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java @@ -81,7 +81,7 @@ public class H2ConsoleAutoConfigurationTests { @Test public void customPathMustBeginWithASlash() { this.thrown.expect(BeanCreationException.class); - this.thrown.expectMessage("Path must start with /"); + this.thrown.expectMessage("Failed to bind properties under 'spring.h2.console'"); this.context.register(H2ConsoleAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "spring.h2.console.enabled:true", "spring.h2.console.path:custom"); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java index 5c09761344..8112cead18 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java @@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerA import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -99,11 +100,12 @@ public class IntegrationAutoConfigurationTests { load(); AnnotationConfigApplicationContext parent = this.context; this.context = new AnnotationConfigApplicationContext(); + ConfigurationPropertySources.attach(this.context.getEnvironment()); this.context.setParent(parent); this.context.register(JmxAutoConfiguration.class, IntegrationAutoConfiguration.class); TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "SPRING_JMX_DEFAULT_DOMAIN=org.foo"); + "spring.jmx.default_domain=org.foo"); this.context.refresh(); assertThat(this.context.getBean(HeaderChannelRegistry.class)).isNotNull(); } @@ -129,7 +131,7 @@ public class IntegrationAutoConfigurationTests { @Test public void customizeJmxDomain() { - load("SPRING_JMX_DEFAULT_DOMAIN=org.foo"); + load("spring.jmx.default_domain=org.foo"); MBeanServer mBeanServer = this.context.getBean(MBeanServer.class); assertDomains(mBeanServer, true, "org.foo"); assertDomains(mBeanServer, false, "org.springframework.integration", @@ -209,6 +211,8 @@ public class IntegrationAutoConfigurationTests { if (configs != null) { ctx.register(configs); } + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(ctx, environment); + ConfigurationPropertySources.attach(ctx.getEnvironment()); ctx.register(JmxAutoConfiguration.class, IntegrationAutoConfiguration.class); ctx.refresh(); this.context = ctx; diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java index 6f84cddfa7..3d4032e84c 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -65,13 +65,12 @@ public class XADataSourceAutoConfigurationTests { public void createFromClass() throws Exception { ApplicationContext context = createContext(FromProperties.class, "spring.datasource.xa.data-source-class-name:org.hsqldb.jdbc.pool.JDBCXADataSource", - "spring.datasource.xa.properties.database-name:test"); + "spring.datasource.xa.properties.login-timeout:123"); context.getBean(DataSource.class); MockXADataSourceWrapper wrapper = context.getBean(MockXADataSourceWrapper.class); JDBCXADataSource dataSource = (JDBCXADataSource) wrapper.getXaDataSource(); assertThat(dataSource).isNotNull(); - assertThat(dataSource.getDatabaseName()).isEqualTo("test"); - + assertThat(dataSource.getLoginTimeout()).isEqualTo(123); } private ApplicationContext createContext(Class configuration, String... env) { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheStandaloneIntegrationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheStandaloneIntegrationTests.java index ab05824c9f..e0debaae12 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheStandaloneIntegrationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheStandaloneIntegrationTests.java @@ -40,7 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; */ @RunWith(SpringRunner.class) @DirtiesContext -@SpringBootTest(webEnvironment = WebEnvironment.NONE, properties = { "env.foo=There", +@SpringBootTest(webEnvironment = WebEnvironment.NONE, properties = { "env.FOO=There", "foo=World" }) public class MustacheStandaloneIntegrationTests { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java index 84f5b3fa81..64a616898d 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java @@ -37,6 +37,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerA import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.test.City; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -131,6 +132,7 @@ public abstract class AbstractJpaAutoConfigurationTests { EnvironmentTestUtils.addEnvironment(context, "spring.jpa.open_in_view:false"); context.register(TestConfiguration.class, EmbeddedDataSourceConfiguration.class, PropertyPlaceholderAutoConfiguration.class, getAutoConfigureClass()); + ConfigurationPropertySources.attach(context.getEnvironment()); context.refresh(); assertThat(getInterceptorBeans(context).length).isEqualTo(0); context.close(); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/OAuth2AutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/OAuth2AutoConfigurationTests.java index 0f26a9c3df..038adcb0ca 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/OAuth2AutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/OAuth2AutoConfigurationTests.java @@ -38,6 +38,7 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2Res import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; @@ -244,6 +245,7 @@ public class OAuth2AutoConfigurationTests { EnvironmentTestUtils.addEnvironment(this.context, "security.oauth2.client.clientId=client", "security.oauth2.client.grantType=client_credentials"); + ConfigurationPropertySources.attach(this.context.getEnvironment()); this.context.refresh(); OAuth2ClientContext bean = this.context.getBean(OAuth2ClientContext.class); assertThat(bean.getAccessTokenRequest()).isNotNull(); @@ -259,6 +261,7 @@ public class OAuth2AutoConfigurationTests { EnvironmentTestUtils.addEnvironment(this.context, "security.oauth2.client.clientId=client", "security.oauth2.client.grantType=client_credentials"); + ConfigurationPropertySources.attach(this.context.getEnvironment()); this.context.refresh(); // The primary context is fine (not session scoped): OAuth2ClientContext bean = this.context.getBean(OAuth2ClientContext.class); @@ -291,6 +294,7 @@ public class OAuth2AutoConfigurationTests { MinimalSecureWebApplication.class); EnvironmentTestUtils.addEnvironment(this.context, "security.oauth2.resource.jwt.keyValue:DEADBEEF"); + ConfigurationPropertySources.attach(this.context.getEnvironment()); this.context.refresh(); assertThat(countBeans(RESOURCE_SERVER_CONFIG)).isEqualTo(1); assertThat(countBeans(AUTHORIZATION_SERVER_CONFIG)).isEqualTo(0); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java index 8d7648b1c6..af40e7e367 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java @@ -100,8 +100,7 @@ public class ResourceServerTokenServicesConfigurationTests { @Test public void useRemoteTokenServices() { EnvironmentTestUtils.addEnvironment(this.environment, - "security.oauth2.resource.tokenInfoUri:http://example.com", - "security.oauth2.resource.clientId=acme"); + "security.oauth2.resource.tokenInfoUri:http://example.com"); this.context = new SpringApplicationBuilder(ResourceConfiguration.class) .environment(this.environment).web(WebApplicationType.NONE).run(); RemoteTokenServices services = this.context.getBean(RemoteTokenServices.class); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sendgrid/SendGridAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sendgrid/SendGridAutoConfigurationTests.java index 74236bfd25..765371d27b 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sendgrid/SendGridAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sendgrid/SendGridAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -22,6 +22,7 @@ import org.junit.After; import org.junit.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -93,6 +94,7 @@ public class SendGridAutoConfigurationTests { private void loadContext(Class additionalConfiguration, String... environment) { this.context = new AnnotationConfigApplicationContext(); EnvironmentTestUtils.addEnvironment(this.context, environment); + ConfigurationPropertySources.attach(this.context.getEnvironment()); this.context.register(SendGridAutoConfiguration.class); if (additionalConfiguration != null) { this.context.register(additionalConfiguration); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java index d657112719..5eb06616b8 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java @@ -91,10 +91,9 @@ public class SessionAutoConfigurationTests extends AbstractSessionAutoConfigurat @Test public void springSessionTimeoutIsNotAValidProperty() { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectMessage("Could not bind"); load("spring.session.store-type=hash-map", "spring.session.timeout=3000"); - MapSessionRepository repository = validateSessionRepository( - MapSessionRepository.class); - assertThat(getSessionTimeout(repository)).isNull(); } @Test diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/FacebookAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/FacebookAutoConfigurationTests.java index f2af3edc66..71715e260a 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/FacebookAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/FacebookAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.social; import org.junit.Test; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.social.facebook.api.Facebook; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -38,6 +39,7 @@ public class FacebookAutoConfigurationTests extends AbstractSocialAutoConfigurat "spring.social.facebook.appId:12345"); EnvironmentTestUtils.addEnvironment(this.context, "spring.social.facebook.appSecret:secret"); + ConfigurationPropertySources.attach(this.context.getEnvironment()); this.context.register(FacebookAutoConfiguration.class); this.context.register(SocialWebAutoConfiguration.class); this.context.refresh(); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/LinkedInAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/LinkedInAutoConfigurationTests.java index eb38524ddd..149b7cad87 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/LinkedInAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/LinkedInAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.social; import org.junit.Test; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.social.linkedin.api.LinkedIn; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -38,6 +39,7 @@ public class LinkedInAutoConfigurationTests extends AbstractSocialAutoConfigurat "spring.social.linkedin.appId:12345"); EnvironmentTestUtils.addEnvironment(this.context, "spring.social.linkedin.appSecret:secret"); + ConfigurationPropertySources.attach(this.context.getEnvironment()); this.context.register(LinkedInAutoConfiguration.class); this.context.register(SocialWebAutoConfiguration.class); this.context.refresh(); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/MultiApiAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/MultiApiAutoConfigurationTests.java index a9c0f9ef7e..a15f7e10f7 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/MultiApiAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/MultiApiAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.social; import org.junit.Test; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.social.facebook.api.Facebook; import org.springframework.social.linkedin.api.LinkedIn; @@ -111,6 +112,7 @@ public class MultiApiAutoConfigurationTests extends AbstractSocialAutoConfigurat private void setupContext(String... environment) { this.context = new AnnotationConfigWebApplicationContext(); EnvironmentTestUtils.addEnvironment(this.context, environment); + ConfigurationPropertySources.attach(this.context.getEnvironment()); this.context.register(TwitterAutoConfiguration.class); this.context.register(FacebookAutoConfiguration.class); this.context.register(LinkedInAutoConfiguration.class); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/TwitterAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/TwitterAutoConfigurationTests.java index e907f33be4..a83ff69035 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/TwitterAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/social/TwitterAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.social; import org.junit.Test; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.social.twitter.api.Twitter; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -38,6 +39,7 @@ public class TwitterAutoConfigurationTests extends AbstractSocialAutoConfigurati "spring.social.twitter.appId:12345"); EnvironmentTestUtils.addEnvironment(this.context, "spring.social.twitter.appSecret:secret"); + ConfigurationPropertySources.attach(this.context.getEnvironment()); this.context.register(TwitterAutoConfiguration.class); this.context.register(SocialWebAutoConfiguration.class); this.context.refresh(); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java index 1f413ca602..68c64f49d1 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java @@ -118,7 +118,7 @@ public class DispatcherServletAutoConfigurationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(DispatcherServletAutoConfiguration.class); - EnvironmentTestUtils.addEnvironment(this.context, "server.servlet_path:/spring"); + EnvironmentTestUtils.addEnvironment(this.context, "server.servlet.path:/spring"); this.context.refresh(); assertThat(this.context.getBean(DispatcherServlet.class)).isNotNull(); ServletRegistrationBean registration = this.context diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index a71d48eb3a..206650331a 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java @@ -332,7 +332,7 @@ public class WebMvcAutoConfigurationTests { @Test public void overrideDateFormat() throws Exception { - load(AllResources.class, "spring.mvc.dateFormat:dd*MM*yyyy"); + load(AllResources.class, "spring.mvc.date-format:dd*MM*yyyy"); FormattingConversionService cs = this.context .getBean(FormattingConversionService.class); Date date = new DateTime(1988, 6, 25, 20, 30).toDate(); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webservices/WebServicesAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webservices/WebServicesAutoConfigurationTests.java index 45f5839e59..24b65f027e 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webservices/WebServicesAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webservices/WebServicesAutoConfigurationTests.java @@ -59,7 +59,7 @@ public class WebServicesAutoConfigurationTests { @Test public void customPathMustBeginWithASlash() { this.thrown.expect(BeanCreationException.class); - this.thrown.expectMessage("Path must start with /"); + this.thrown.expectMessage("Failed to bind properties under 'spring.webservices'"); load(WebServicesAutoConfiguration.class, "spring.webservices.path=invalid"); } From 866cf1dda747a181170f594da834e6a79970189d Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 25 Apr 2017 10:52:41 -0700 Subject: [PATCH 10/19] Use new configuration properties in actuator Update `spring-boot-actuator` to use the new configuration properties support. See gh-9000 --- .../EndpointMBeanExportAutoConfiguration.java | 14 ++--- .../EndpointWebMvcAutoConfiguration.java | 19 +++---- ...tWebMvcManagementContextConfiguration.java | 5 +- .../JolokiaAutoConfiguration.java | 7 +-- .../OnEnabledEndpointElementCondition.java | 18 +++--- ...CloudFoundryActuatorAutoConfiguration.java | 7 +-- .../condition/OnEnabledEndpointCondition.java | 18 +++--- .../endpoint/mvc/HealthMvcEndpoint.java | 57 ++++++++----------- .../info/EnvironmentInfoContributor.java | 18 ++++-- .../info/InfoPropertiesInfoContributor.java | 17 +++++- .../EndpointAutoConfigurationTests.java | 23 ++++---- .../EndpointWebMvcAutoConfigurationTests.java | 8 ++- ...FoundryActuatorAutoConfigurationTests.java | 2 + .../endpoint/mvc/HealthMvcEndpointTests.java | 22 +++---- 14 files changed, 117 insertions(+), 118 deletions(-) diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java index 7ff646c159..91067c9beb 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java @@ -35,7 +35,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; @@ -50,6 +49,7 @@ import org.springframework.util.StringUtils; * * @author Christian Dupuis * @author Andy Wilkinson + * @author Madhura Bhave */ @Configuration @Conditional(JmxEnabledCondition.class) @@ -103,8 +103,10 @@ public class EndpointMBeanExportAutoConfiguration { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - boolean jmxEnabled = isEnabled(context, "spring.jmx."); - boolean jmxEndpointsEnabled = isEnabled(context, "endpoints.jmx."); + boolean jmxEnabled = context.getEnvironment() + .getProperty("spring.jmx.enabled", Boolean.class, true); + boolean jmxEndpointsEnabled = context.getEnvironment() + .getProperty("endpoints.jmx.enabled", Boolean.class, true); if (jmxEnabled && jmxEndpointsEnabled) { return ConditionOutcome.match( ConditionMessage.forCondition("JMX Enabled").found("properties") @@ -114,12 +116,6 @@ public class EndpointMBeanExportAutoConfiguration { .because("spring.jmx.enabled or endpoints.jmx.enabled is not set")); } - private boolean isEnabled(ConditionContext context, String prefix) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), prefix); - return resolver.getProperty("enabled", Boolean.class, true); - } - } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java index 02d8d0578f..2370a3e5fb 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java @@ -49,7 +49,6 @@ import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; @@ -89,6 +88,7 @@ import org.springframework.web.servlet.DispatcherServlet; * @author Johannes Edmeier * @author Eddú Meléndez * @author Venil Noronha + * @author Madhura Bhave */ @Configuration @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) @@ -133,9 +133,9 @@ public class EndpointWebMvcAutoConfiguration @Override public void afterSingletonsInstantiated() { ManagementServerPort managementPort = ManagementServerPort.DIFFERENT; + Environment environment = this.applicationContext.getEnvironment(); if (this.applicationContext instanceof WebApplicationContext) { - managementPort = ManagementServerPort - .get(this.applicationContext.getEnvironment()); + managementPort = ManagementServerPort.get(environment); } if (managementPort == ManagementServerPort.DIFFERENT) { if (this.applicationContext instanceof ServletWebServerApplicationContext @@ -150,17 +150,14 @@ public class EndpointWebMvcAutoConfiguration } } if (managementPort == ManagementServerPort.SAME) { - if (new RelaxedPropertyResolver(this.applicationContext.getEnvironment(), - "management.ssl.").getProperty("enabled") != null) { + if (environment.getProperty("management.ssl.enabled") != null) { throw new IllegalStateException( "Management-specific SSL cannot be configured as the management " + "server is not listening on a separate port"); } - if (this.applicationContext - .getEnvironment() instanceof ConfigurableEnvironment) { + if (environment instanceof ConfigurableEnvironment) { addLocalManagementPortPropertyAlias( - (ConfigurableEnvironment) this.applicationContext - .getEnvironment()); + (ConfigurableEnvironment) environment); } } } @@ -356,9 +353,7 @@ public class EndpointWebMvcAutoConfiguration } private static Integer getPortProperty(Environment environment, String prefix) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, - prefix); - return resolver.getProperty("port", Integer.class); + return environment.getProperty(prefix + "port", Integer.class); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java index c7d30a745c..b1391d287e 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java @@ -47,7 +47,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; @@ -64,6 +63,7 @@ import org.springframework.web.cors.CorsConfiguration; * @author Dave Syer * @author Ben Hale * @author Vedran Pavic + * @author Madhura Bhave * @since 1.3.0 */ @ManagementContextConfiguration @@ -225,8 +225,7 @@ public class EndpointWebMvcManagementContextConfiguration { return ConditionOutcome .match(message.found("logging.path").items(config)); } - config = new RelaxedPropertyResolver(environment, "endpoints.logfile.") - .getProperty("external-file"); + config = environment.getProperty("endpoints.logfile.external-file"); if (StringUtils.hasText(config)) { return ConditionOutcome.match( message.found("endpoints.logfile.external-file").items(config)); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/JolokiaAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/JolokiaAutoConfiguration.java index e42923143d..dd276abbba 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/JolokiaAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/JolokiaAutoConfiguration.java @@ -33,7 +33,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; @@ -60,6 +59,7 @@ import org.springframework.web.servlet.mvc.ServletWrappingController; * @author Christian Dupuis * @author Dave Syer * @author Andy Wilkinson + * @author Madhura Bhave */ @Configuration @ConditionalOnWebApplication(type = Type.SERVLET) @@ -108,9 +108,8 @@ public class JolokiaAutoConfiguration { private boolean isEnabled(ConditionContext context, String prefix, boolean defaultValue) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), prefix); - return resolver.getProperty("enabled", Boolean.class, defaultValue); + return context.getEnvironment().getProperty(prefix + "enabled", Boolean.class, + defaultValue); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/OnEnabledEndpointElementCondition.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/OnEnabledEndpointElementCondition.java index 04196b84b7..3acc0297c1 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/OnEnabledEndpointElementCondition.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/OnEnabledEndpointElementCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -21,9 +21,9 @@ import java.lang.annotation.Annotation; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; /** @@ -31,6 +31,7 @@ import org.springframework.core.type.AnnotatedTypeMetadata; * {@code defaults} name or individually via the name of the element. * * @author Stephane Nicoll + * @author Madhura Bhave */ abstract class OnEnabledEndpointElementCondition extends SpringBootCondition { @@ -59,10 +60,10 @@ abstract class OnEnabledEndpointElementCondition extends SpringBootCondition { protected ConditionOutcome getEndpointOutcome(ConditionContext context, String endpointName) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), this.prefix + endpointName + "."); - if (resolver.containsProperty("enabled")) { - boolean match = resolver.getProperty("enabled", Boolean.class, true); + Environment environment = context.getEnvironment(); + String enabledProperty = this.prefix + endpointName + ".enabled"; + if (environment.containsProperty(enabledProperty)) { + boolean match = environment.getProperty(enabledProperty, Boolean.class, true); return new ConditionOutcome(match, ConditionMessage.forCondition(this.annotationType).because( this.prefix + endpointName + ".enabled is " + match)); @@ -71,9 +72,8 @@ abstract class OnEnabledEndpointElementCondition extends SpringBootCondition { } protected ConditionOutcome getDefaultEndpointsOutcome(ConditionContext context) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), this.prefix + "defaults."); - boolean match = Boolean.valueOf(resolver.getProperty("enabled", "true")); + boolean match = Boolean.valueOf(context.getEnvironment() + .getProperty(this.prefix + "defaults.enabled", "true")); return new ConditionOutcome(match, ConditionMessage.forCondition(this.annotationType).because( this.prefix + "defaults.enabled is considered " + match)); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java index 4d7cc6b3df..135d8567f9 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java @@ -30,7 +30,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.IgnoredRequestCustomizer; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; @@ -84,11 +83,9 @@ public class CloudFoundryActuatorAutoConfiguration { private CloudFoundrySecurityService getCloudFoundrySecurityService( RestTemplateBuilder restTemplateBuilder, Environment environment) { - RelaxedPropertyResolver cloudFoundryProperties = new RelaxedPropertyResolver( - environment, "management.cloudfoundry."); String cloudControllerUrl = environment.getProperty("vcap.application.cf_api"); - boolean skipSslValidation = cloudFoundryProperties - .getProperty("skip-ssl-validation", Boolean.class, false); + boolean skipSslValidation = environment.getProperty( + "management.cloudfoundry.skip-ssl-validation", Boolean.class, false); return cloudControllerUrl == null ? null : new CloudFoundrySecurityService(restTemplateBuilder, cloudControllerUrl, skipSslValidation); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/condition/OnEnabledEndpointCondition.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/condition/OnEnabledEndpointCondition.java index ffd903100a..6f0df8ea88 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/condition/OnEnabledEndpointCondition.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/condition/OnEnabledEndpointCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -19,16 +19,17 @@ package org.springframework.boot.actuate.condition; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; /** * {@link Condition} that checks whether or not an endpoint is enabled. * * @author Andy Wilkinson + * @author Madhura Bhave */ class OnEnabledEndpointCondition extends SpringBootCondition { @@ -49,10 +50,10 @@ class OnEnabledEndpointCondition extends SpringBootCondition { private ConditionOutcome determineEndpointOutcome(String endpointName, boolean enabledByDefault, ConditionContext context) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), "endpoints." + endpointName + "."); - if (resolver.containsProperty("enabled") || !enabledByDefault) { - boolean match = resolver.getProperty("enabled", Boolean.class, + Environment environment = context.getEnvironment(); + String enabledProperty = "endpoints." + endpointName + ".enabled"; + if (environment.containsProperty(enabledProperty) || !enabledByDefault) { + boolean match = environment.getProperty(enabledProperty, Boolean.class, enabledByDefault); ConditionMessage message = ConditionMessage .forCondition(ConditionalOnEnabledEndpoint.class, @@ -64,9 +65,8 @@ class OnEnabledEndpointCondition extends SpringBootCondition { } private ConditionOutcome determineAllEndpointsOutcome(ConditionContext context) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), "endpoints."); - boolean match = Boolean.valueOf(resolver.getProperty("enabled", "true")); + boolean match = Boolean.valueOf( + context.getEnvironment().getProperty("endpoints.enabled", "true")); ConditionMessage message = ConditionMessage .forCondition(ConditionalOnEnabledEndpoint.class) .because("All endpoints are " + (match ? "enabled" : "disabled") diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java index fc86febd32..70aab222de 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.endpoint.mvc; import java.security.Principal; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -27,18 +28,13 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.boot.actuate.endpoint.HealthEndpoint; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.bind.RelaxedNames; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ResponseBody; /** @@ -53,8 +49,9 @@ import org.springframework.web.bind.annotation.ResponseBody; * @since 1.1.0 */ @ConfigurationProperties(prefix = "endpoints.health") -public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter - implements EnvironmentAware { +public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter { + + private static final List DEFAULT_ROLES = Arrays.asList("ROLE_ACTUATOR"); private final boolean secure; @@ -62,8 +59,6 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter statusMapping = new HashMap<>(); - private RelaxedPropertyResolver securityPropertyResolver; - private long lastAccess = 0; private Health cached; @@ -73,15 +68,16 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter(DEFAULT_ROLES)); } public HealthMvcEndpoint(HealthEndpoint delegate, boolean secure, List roles) { super(delegate); + Assert.notNull(roles, "Roles must not be null"); this.secure = secure; - setupDefaultStatusMapping(); this.roles = roles; + setupDefaultStatusMapping(); } private void setupDefaultStatusMapping() { @@ -89,12 +85,6 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter code.equals(getUniformValue(key))) + .map(this.statusMapping::get).findFirst().orElse(null); } return null; } + private String getUniformValue(String code) { + if (code == null) { + return null; + } + StringBuilder builder = new StringBuilder(); + for (char ch : code.toCharArray()) { + if (Character.isAlphabetic(ch) || Character.isDigit(ch)) { + builder.append(Character.toLowerCase(ch)); + } + } + return builder.toString(); + } + private Health getHealth(HttpServletRequest request, Principal principal) { long accessTime = System.currentTimeMillis(); if (isCacheStale(accessTime)) { @@ -207,13 +206,7 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter getRoles() { - if (this.roles != null) { - return this.roles; - } - String[] roles = StringUtils.commaDelimitedListToStringArray( - this.securityPropertyResolver.getProperty("roles", "ROLE_ACTUATOR")); - roles = StringUtils.trimArrayElements(roles); - return Arrays.asList(roles); + return this.roles; } private boolean isSpringSecurityAuthentication(Principal principal) { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/EnvironmentInfoContributor.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/EnvironmentInfoContributor.java index d0ffb41168..c061e5e0ee 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/EnvironmentInfoContributor.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/EnvironmentInfoContributor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -16,7 +16,10 @@ package org.springframework.boot.actuate.info; -import org.springframework.boot.bind.PropertySourcesBinder; +import java.util.Map; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.core.env.ConfigurableEnvironment; /** @@ -24,19 +27,24 @@ import org.springframework.core.env.ConfigurableEnvironment; * * @author Meang Akira Tanaka * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.4.0 */ public class EnvironmentInfoContributor implements InfoContributor { - private final PropertySourcesBinder binder; + private static final Bindable> STRING_OBJECT_MAP = Bindable + .mapOf(String.class, Object.class); + + private final ConfigurableEnvironment environment; public EnvironmentInfoContributor(ConfigurableEnvironment environment) { - this.binder = new PropertySourcesBinder(environment); + this.environment = environment; } @Override public void contribute(Info.Builder builder) { - builder.withDetails(this.binder.extractAll("info")); + Binder binder = Binder.get(this.environment); + binder.bind("info", STRING_OBJECT_MAP).ifBound(builder::withDetails); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/InfoPropertiesInfoContributor.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/InfoPropertiesInfoContributor.java index 6f30be29f2..383c7472b9 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/InfoPropertiesInfoContributor.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/InfoPropertiesInfoContributor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -17,11 +17,15 @@ package org.springframework.boot.actuate.info; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; -import org.springframework.boot.bind.PropertySourcesBinder; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.info.InfoProperties; +import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.util.StringUtils; @@ -30,11 +34,15 @@ import org.springframework.util.StringUtils; * * @param the type of the {@link InfoProperties} to expose * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.4.0 */ public abstract class InfoPropertiesInfoContributor implements InfoContributor { + private static final Bindable> STRING_OBJECT_MAP = Bindable + .mapOf(String.class, Object.class); + private final T properties; private final Mode mode; @@ -85,7 +93,10 @@ public abstract class InfoPropertiesInfoContributor * @return the raw content */ protected Map extractContent(PropertySource propertySource) { - return new PropertySourcesBinder(propertySource).extractAll(""); + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(propertySource); + return new Binder(ConfigurationPropertySources.get(sources)) + .bind("", STRING_OBJECT_MAP).orElseGet(LinkedHashMap::new); } /** diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java index 0c2b398feb..e85f8a9b96 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java @@ -21,7 +21,6 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Properties; import javax.sql.DataSource; @@ -55,14 +54,16 @@ import org.springframework.boot.autoconfigure.info.ProjectInfoProperties; import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; -import org.springframework.boot.bind.PropertySourcesBinder; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; import org.springframework.core.annotation.Order; -import org.springframework.core.env.PropertiesPropertySource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.validation.BindException; @@ -310,17 +311,19 @@ public class EndpointAutoConfigurationTests { private static class GitFullInfoContributor implements InfoContributor { + private static final ResolvableType STRING_OBJECT_MAP = ResolvableType + .forClassWithGenerics(Map.class, String.class, Object.class); + private Map content = new LinkedHashMap<>(); GitFullInfoContributor(Resource location) throws BindException, IOException { - if (location.exists()) { - Properties gitInfoProperties = PropertiesLoaderUtils - .loadProperties(location); - PropertiesPropertySource gitPropertySource = new PropertiesPropertySource( - "git", gitInfoProperties); - this.content = new PropertySourcesBinder(gitPropertySource) - .extractAll("git"); + if (!location.exists()) { + return; } + MapConfigurationPropertySource source = new MapConfigurationPropertySource( + PropertiesLoaderUtils.loadProperties(location)); + new Binder(source).bind("info", + Bindable.of(STRING_OBJECT_MAP).withExistingValue(this.content)); } @Override diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java index df17a03424..e8c4734539 100755 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java @@ -62,6 +62,7 @@ import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactor import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; import org.springframework.boot.context.event.ApplicationFailedEvent; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.boot.testutil.Matched; @@ -357,7 +358,7 @@ public class EndpointWebMvcAutoConfigurationTests { @Test public void contextPath() throws Exception { EnvironmentTestUtils.addEnvironment(this.applicationContext, - "management.contextPath:/test", "management.security.enabled:false"); + "management.context-path:/test", "management.security.enabled:false"); this.applicationContext.register(RootConfig.class, EndpointConfig.class, PropertyPlaceholderAutoConfiguration.class, JacksonAutoConfiguration.class, @@ -448,7 +449,7 @@ public class EndpointWebMvcAutoConfigurationTests { this.applicationContext.register(RootConfig.class, BaseConfiguration.class, EndpointWebMvcAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.applicationContext, - "ENDPOINTS_ENABLED:false"); + "endpoints.enabled:false"); this.applicationContext.refresh(); assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class)).isEmpty(); } @@ -646,9 +647,10 @@ public class EndpointWebMvcAutoConfigurationTests { throws Exception { this.applicationContext.register(LoggingConfig.class, RootConfig.class, BaseConfiguration.class, EndpointWebMvcAutoConfiguration.class); + ConfigurationPropertySources.attach(this.applicationContext.getEnvironment()); EnvironmentTestUtils.addEnvironment(this.applicationContext, "endpoints.enabled:false", - String.format("endpoints_%s_enabled:true", name)); + String.format("endpoints.%s.enabled:true", name)); this.applicationContext.refresh(); assertThat(this.applicationContext.getBeansOfType(type)).hasSize(1); } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java index c177119879..9b9697cea9 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java @@ -34,6 +34,7 @@ import org.springframework.boot.autoconfigure.security.IgnoredRequestCustomizer; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; @@ -120,6 +121,7 @@ public class CloudFoundryActuatorAutoConfigurationTests { public void skipSslValidation() throws Exception { EnvironmentTestUtils.addEnvironment(this.context, "management.cloudfoundry.skipSslValidation:true"); + ConfigurationPropertySources.attach(this.context.getEnvironment()); this.context.refresh(); CloudFoundryEndpointHandlerMapping handlerMapping = getHandlerMapping(); Object interceptor = ReflectionTestUtils.getField(handlerMapping, diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpointTests.java index 6f0476cc80..998e5f7584 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpointTests.java @@ -16,8 +16,10 @@ package org.springframework.boot.actuate.endpoint.mvc; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Set; import javax.servlet.http.HttpServletRequest; @@ -28,11 +30,8 @@ import org.junit.Test; import org.springframework.boot.actuate.endpoint.HealthEndpoint; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.PropertySource; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockServletContext; import org.springframework.security.core.Authentication; @@ -54,9 +53,8 @@ import static org.mockito.Mockito.mock; */ public class HealthMvcEndpointTests { - private static final PropertySource SECURITY_ROLES = new MapPropertySource("test", - Collections.singletonMap("management.security.roles", - "HERO")); + private static final List SECURITY_ROLES = new ArrayList<>( + Arrays.asList("HERO")); private HttpServletRequest request = new MockHttpServletRequest(); @@ -64,8 +62,6 @@ public class HealthMvcEndpointTests { private HealthMvcEndpoint mvc = null; - private MockEnvironment environment; - private HttpServletRequest defaultUser = createAuthenticationRequest("ROLE_ACTUATOR"); private HttpServletRequest hero = createAuthenticationRequest("HERO"); @@ -81,8 +77,6 @@ public class HealthMvcEndpointTests { this.endpoint = mock(HealthEndpoint.class); given(this.endpoint.isEnabled()).willReturn(true); this.mvc = new HealthMvcEndpoint(this.endpoint); - this.environment = new MockEnvironment(); - this.mvc.setEnvironment(this.environment); } @Test @@ -123,7 +117,7 @@ public class HealthMvcEndpointTests { public void customMappingWithRelaxedName() { given(this.endpoint.invoke()) .willReturn(new Health.Builder().outOfService().build()); - this.mvc.setStatusMapping(Collections.singletonMap("out-of-service", + this.mvc.setStatusMapping(Collections.singletonMap("out-OF-serVice", HttpStatus.INTERNAL_SERVER_ERROR)); Object result = this.mvc.invoke(this.request, null); assertThat(result instanceof ResponseEntity).isTrue(); @@ -165,7 +159,7 @@ public class HealthMvcEndpointTests { @Test public void rightAuthorityPresentShouldExposeDetails() throws Exception { - this.environment.getPropertySources().addLast(SECURITY_ROLES); + this.mvc = new HealthMvcEndpoint(this.endpoint, true, SECURITY_ROLES); Authentication principal = mock(Authentication.class); Set authorities = Collections .singleton(new SimpleGrantedAuthority("HERO")); @@ -180,7 +174,7 @@ public class HealthMvcEndpointTests { @Test public void customRolePresentShouldExposeDetails() { - this.environment.getPropertySources().addLast(SECURITY_ROLES); + this.mvc = new HealthMvcEndpoint(this.endpoint, true, SECURITY_ROLES); given(this.endpoint.invoke()) .willReturn(new Health.Builder().up().withDetail("foo", "bar").build()); Object result = this.mvc.invoke(this.hero, null); @@ -191,7 +185,7 @@ public class HealthMvcEndpointTests { @Test public void customRoleShouldNotExposeDetailsForDefaultRole() { - this.environment.getPropertySources().addLast(SECURITY_ROLES); + this.mvc = new HealthMvcEndpoint(this.endpoint, true, SECURITY_ROLES); given(this.endpoint.invoke()) .willReturn(new Health.Builder().up().withDetail("foo", "bar").build()); Object result = this.mvc.invoke(this.defaultUser, null); From 7efbedc0dc54ceb8025739c3b2e1771d613b4112 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 25 Apr 2017 10:53:01 -0700 Subject: [PATCH 11/19] Use new configuration properties in devtools Update `spring-boot-devtools` to use the new configuration properties support. See gh-9000 --- .../env/DevToolsPropertyDefaultsPostProcessor.java | 6 ++---- .../client/LocalDebugPortAvailableCondition.java | 13 +++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/env/DevToolsPropertyDefaultsPostProcessor.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/env/DevToolsPropertyDefaultsPostProcessor.java index b6118cf3bc..28ef8e409f 100755 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/env/DevToolsPropertyDefaultsPostProcessor.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/env/DevToolsPropertyDefaultsPostProcessor.java @@ -21,7 +21,6 @@ import java.util.HashMap; import java.util.Map; import org.springframework.boot.SpringApplication; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.devtools.restart.Restarter; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.core.Ordered; @@ -37,6 +36,7 @@ import org.springframework.core.env.PropertySource; * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave * @since 1.3.0 */ @Order(Ordered.LOWEST_PRECEDENCE) @@ -90,9 +90,7 @@ public class DevToolsPropertyDefaultsPostProcessor implements EnvironmentPostPro } private boolean isRemoteRestartEnabled(Environment environment) { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, - "spring.devtools.remote."); - return resolver.containsProperty("secret"); + return environment.containsProperty("spring.devtools.remote.secret"); } } diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/LocalDebugPortAvailableCondition.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/LocalDebugPortAvailableCondition.java index 5b8d6606f1..d8f1e3051a 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/LocalDebugPortAvailableCondition.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/LocalDebugPortAvailableCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -21,7 +21,6 @@ import javax.net.ServerSocketFactory; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.devtools.autoconfigure.RemoteDevToolsProperties; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; @@ -30,6 +29,7 @@ import org.springframework.core.type.AnnotatedTypeMetadata; * Condition used to check that the actual local port is available. * * @author Phillip Webb + * @author Madhura Bhave */ class LocalDebugPortAvailableCondition extends SpringBootCondition { @@ -38,12 +38,9 @@ class LocalDebugPortAvailableCondition extends SpringBootCondition { AnnotatedTypeMetadata metadata) { ConditionMessage.Builder message = ConditionMessage .forCondition("Local Debug Port Condition"); - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( - context.getEnvironment(), "spring.devtools.remote.debug."); - Integer port = resolver.getProperty("local-port", Integer.class); - if (port == null) { - port = RemoteDevToolsProperties.Debug.DEFAULT_LOCAL_PORT; - } + Integer port = context.getEnvironment().getProperty( + "spring.devtools.remote.debug.local-port", Integer.class, + RemoteDevToolsProperties.Debug.DEFAULT_LOCAL_PORT); if (isPortAvailable(port)) { return ConditionOutcome.match(message.foundExactly("local debug port")); } From 829ab5975746d656bdffc5d940e124ca3e0754e0 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 25 Apr 2017 10:53:14 -0700 Subject: [PATCH 12/19] Use new configuration properties in samples Update `spring-boot-samples` to use the new configuration properties support. See gh-9000 --- .../java/sample/propertyvalidation/SampleProperties.java | 4 +++- .../SamplePropertyValidationApplicationTests.java | 7 +++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/spring-boot-samples/spring-boot-sample-property-validation/src/main/java/sample/propertyvalidation/SampleProperties.java b/spring-boot-samples/spring-boot-sample-property-validation/src/main/java/sample/propertyvalidation/SampleProperties.java index e990edf394..e5b6af2389 100644 --- a/spring-boot-samples/spring-boot-sample-property-validation/src/main/java/sample/propertyvalidation/SampleProperties.java +++ b/spring-boot-samples/spring-boot-sample-property-validation/src/main/java/sample/propertyvalidation/SampleProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * 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. @@ -18,9 +18,11 @@ package sample.propertyvalidation; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; @Component @ConfigurationProperties(prefix = "sample") +@Validated public class SampleProperties { /** diff --git a/spring-boot-samples/spring-boot-sample-property-validation/src/test/java/sample/propertyvalidation/SamplePropertyValidationApplicationTests.java b/spring-boot-samples/spring-boot-sample-property-validation/src/test/java/sample/propertyvalidation/SamplePropertyValidationApplicationTests.java index f1d2ad4d7b..b77c528fd8 100644 --- a/spring-boot-samples/spring-boot-sample-property-validation/src/test/java/sample/propertyvalidation/SamplePropertyValidationApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-property-validation/src/test/java/sample/propertyvalidation/SamplePropertyValidationApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -63,7 +63,7 @@ public class SamplePropertyValidationApplicationTests { EnvironmentTestUtils.addEnvironment(this.context, "sample.host:xxxxxx", "sample.port:9090"); this.thrown.expect(BeanCreationException.class); - this.thrown.expectMessage("xxxxxx"); + this.thrown.expectMessage("Failed to bind properties under 'sample'"); this.context.refresh(); } @@ -71,8 +71,7 @@ public class SamplePropertyValidationApplicationTests { public void bindNullHost() { this.context.register(SamplePropertyValidationApplication.class); this.thrown.expect(BeanCreationException.class); - this.thrown.expectMessage("null"); - this.thrown.expectMessage("host"); + this.thrown.expectMessage("Failed to bind properties under 'sample'"); this.context.refresh(); } From fa45cd577256387742b059d56dc5590dad578b82 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 25 Apr 2017 10:51:19 -0700 Subject: [PATCH 13/19] Extended BindFailureAnalyzer for new exceptions Extend `BindFailureAnalyzer` to support the exceptions thrown by the new configuration properties `Binder`. Origin information is now also reported when available. Closes gh-8934 --- .../analyzer/BindFailureAnalyzer.java | 84 ++++++++++++-- .../analyzer/BindFailureAnalyzerTests.java | 106 +++++++++++++++++- 2 files changed, 177 insertions(+), 13 deletions(-) diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java index 384dc79073..f2888d25d4 100644 --- a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java @@ -16,10 +16,15 @@ package org.springframework.boot.diagnostics.analyzer; +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.UnboundConfigurationPropertiesException; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.boot.context.properties.bind.validation.ValidationErrors; +import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; import org.springframework.boot.diagnostics.FailureAnalysis; -import org.springframework.util.CollectionUtils; -import org.springframework.validation.BindException; +import org.springframework.boot.origin.Origin; +import org.springframework.util.StringUtils; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; @@ -34,22 +39,83 @@ class BindFailureAnalyzer extends AbstractFailureAnalyzer { @Override protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) { - if (CollectionUtils.isEmpty(cause.getAllErrors())) { + if (cause.getCause() instanceof BindValidationException) { + return analyzeBindValidationException(cause, + (BindValidationException) cause.getCause()); + } + else if (cause.getCause() instanceof UnboundConfigurationPropertiesException) { + return analyzeUnboundConfigurationPropertiesException(cause, + (UnboundConfigurationPropertiesException) cause.getCause()); + } + return analyzeGenericBindException(cause); + } + + private FailureAnalysis analyzeBindValidationException(BindException cause, + BindValidationException validationException) { + ValidationErrors errors = validationException.getValidationErrors(); + if (!errors.hasErrors()) { return null; } StringBuilder description = new StringBuilder( String.format("Binding to target %s failed:%n", cause.getTarget())); - for (ObjectError error : cause.getAllErrors()) { + for (ObjectError error : errors) { if (error instanceof FieldError) { - FieldError fieldError = (FieldError) error; - description.append(String.format("%n Property: %s", - cause.getObjectName() + "." + fieldError.getField())); - description.append( - String.format("%n Value: %s", fieldError.getRejectedValue())); + appendFieldError(description, (FieldError) error); } description.append( String.format("%n Reason: %s%n", error.getDefaultMessage())); } + return getFailureAnalysis(description, cause); + } + + private void appendFieldError(StringBuilder description, FieldError error) { + Origin origin = Origin.from(error); + description.append(String.format("%n Property: %s", + error.getObjectName() + "." + error.getField())); + description.append(String.format("%n Value: %s", error.getRejectedValue())); + if (origin != null) { + description.append(String.format("%n Origin: %s", origin)); + } + } + + private FailureAnalysis analyzeUnboundConfigurationPropertiesException( + BindException cause, UnboundConfigurationPropertiesException exception) { + StringBuilder description = new StringBuilder( + String.format("Binding to target %s failed:%n", cause.getTarget())); + for (ConfigurationProperty property : exception.getUnboundProperties()) { + buildDescription(description, property); + description.append(String.format("%n Reason: %s", exception.getMessage())); + } + return getFailureAnalysis(description, cause); + } + + private FailureAnalysis analyzeGenericBindException(BindException cause) { + StringBuilder description = new StringBuilder( + String.format("Binding to target %s failed:%n", cause.getTarget())); + ConfigurationProperty property = cause.getProperty(); + buildDescription(description, property); + description.append(String.format("%n Reason: %s", getMessage(cause))); + return getFailureAnalysis(description, cause); + } + + private void buildDescription(StringBuilder description, + ConfigurationProperty property) { + if (property != null) { + description.append(String.format("%n Property: %s", property.getName())); + description.append(String.format("%n Value: %s", property.getValue())); + description.append(String.format("%n Origin: %s", property.getOrigin())); + } + } + + private String getMessage(BindException cause) { + if (cause.getCause() != null + && StringUtils.hasText(cause.getCause().getMessage())) { + return cause.getCause().getMessage(); + } + return cause.getMessage(); + } + + private FailureAnalysis getFailureAnalysis(Object description, BindException cause) { return new FailureAnalysis(description.toString(), "Update your application's configuration", cause); } diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java index 4e00c9ac98..978f4f5b7e 100644 --- a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java @@ -16,7 +16,10 @@ package org.springframework.boot.diagnostics.analyzer; +import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; import javax.validation.Valid; import javax.validation.constraints.Min; @@ -32,6 +35,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.diagnostics.FailureAnalysis; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; @@ -76,20 +81,60 @@ public class BindFailureAnalyzerTests { .contains("Reason: This object could not be bound."); } + @Test + public void bindExceptionWithOriginDueToValidationFailure() throws Exception { + FailureAnalysis analysis = performAnalysis( + FieldValidationFailureConfiguration.class, "test.foo.value=4"); + assertThat(analysis.getDescription()) + .contains("Origin: \"test.foo.value\" from property source \"test\""); + } + + @Test + public void bindExceptionDueToUnboundElements() throws Exception { + FailureAnalysis analysis = performAnalysis( + UnboundElementsFailureConfiguration.class, "test.foo.listValue[0]=hello", + "test.foo.listValue[2]=world"); + assertThat(analysis.getDescription()).contains(failure("test.foo.listvalue[2]", + "world", "\"test.foo.listValue[2]\" from property source \"test\"", + "The elements [test.foo.listvalue[2]] were left unbound.")); + } + + @Test + public void bindExceptionDueToOtherFailure() throws Exception { + FailureAnalysis analysis = performAnalysis(GenericFailureConfiguration.class, + "test.foo.value=${BAR}"); + assertThat(analysis.getDescription()).contains(failure("test.foo.value", "${BAR}", + "\"test.foo.value\" from property source \"test\"", + "Could not resolve placeholder 'BAR' in value \"${BAR}\"")); + } + private static String failure(String property, String value, String reason) { return String.format("Property: %s%n Value: %s%n Reason: %s", property, value, reason); } - private FailureAnalysis performAnalysis(Class configuration) { - BeanCreationException failure = createFailure(configuration); + private static String failure(String property, String value, String origin, + String reason) { + return String.format( + "Property: %s%n Value: %s%n Origin: %s%n Reason: %s", property, + value, origin, reason); + } + + private FailureAnalysis performAnalysis(Class configuration, + String... environment) { + BeanCreationException failure = createFailure(configuration, environment); assertThat(failure).isNotNull(); return new BindFailureAnalyzer().analyze(failure); } - private BeanCreationException createFailure(Class configuration) { + private BeanCreationException createFailure(Class configuration, + String... environment) { try { - new AnnotationConfigApplicationContext(configuration).close(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + addEnvironment(context, environment); + context.register(configuration); + context.refresh(); + context.close(); return null; } catch (BeanCreationException ex) { @@ -97,6 +142,19 @@ public class BindFailureAnalyzerTests { } } + private void addEnvironment(AnnotationConfigApplicationContext context, + String[] environment) { + MutablePropertySources sources = context.getEnvironment().getPropertySources(); + Map map = new HashMap<>(); + for (String pair : environment) { + int index = pair.indexOf("="); + String key = pair.substring(0, index > 0 ? index : pair.length()); + String value = index > 0 ? pair.substring(index + 1) : ""; + map.put(key.trim(), value.trim()); + } + sources.addFirst(new MapPropertySource("test", map)); + } + @EnableConfigurationProperties(FieldValidationFailureProperties.class) static class FieldValidationFailureConfiguration { @@ -107,6 +165,16 @@ public class BindFailureAnalyzerTests { } + @EnableConfigurationProperties(UnboundElementsFailureProperties.class) + static class UnboundElementsFailureConfiguration { + + } + + @EnableConfigurationProperties(GenericFailureProperties.class) + static class GenericFailureConfiguration { + + } + @ConfigurationProperties("test.foo") @Validated static class FieldValidationFailureProperties { @@ -162,6 +230,7 @@ public class BindFailureAnalyzerTests { } @ConfigurationProperties("foo.bar") + @Validated static class ObjectErrorFailureProperties implements Validator { @Override @@ -176,4 +245,33 @@ public class BindFailureAnalyzerTests { } + @ConfigurationProperties("test.foo") + static class UnboundElementsFailureProperties { + + private List listValue; + + public List getListValue() { + return this.listValue; + } + + public void setListValue(List listValue) { + this.listValue = listValue; + } + } + + @ConfigurationProperties("test.foo") + static class GenericFailureProperties { + + private String value; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + } + } From df9d2bc9f47381a2f9e6435f825eb71e63840393 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 25 Apr 2017 10:47:26 -0700 Subject: [PATCH 14/19] Remove @ConditionalOnProperty 'relaxed' attribute Remove the `relaxed` attribute from `@ConditionalOnProperty` and instead rely on the direct configuration property source relaxed name support. Closes gh-9003 --- .../condition/ConditionalOnProperty.java | 8 +-- .../condition/OnPropertyCondition.java | 9 +-- .../condition/ConditionalOnPropertyTests.java | 62 +++++-------------- 3 files changed, 16 insertions(+), 63 deletions(-) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java index de09c90acf..f9db375c29 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -122,10 +122,4 @@ public @interface ConditionalOnProperty { */ boolean matchIfMissing() default false; - /** - * If relaxed names should be checked. Defaults to {@code true}. - * @return if relaxed names are used - */ - boolean relaxedNames() default true; - } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java index d5b8ace68e..c084a7840b 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java @@ -23,7 +23,6 @@ import java.util.Map; import java.util.Map.Entry; import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.Ordered; @@ -121,8 +120,6 @@ class OnPropertyCondition extends SpringBootCondition { private final String[] names; - private final boolean relaxedNames; - private final boolean matchIfMissing; Spec(AnnotationAttributes annotationAttributes) { @@ -133,7 +130,6 @@ class OnPropertyCondition extends SpringBootCondition { this.prefix = prefix; this.havingValue = annotationAttributes.getString("havingValue"); this.names = getNames(annotationAttributes); - this.relaxedNames = annotationAttributes.getBoolean("relaxedNames"); this.matchIfMissing = annotationAttributes.getBoolean("matchIfMissing"); } @@ -149,11 +145,8 @@ class OnPropertyCondition extends SpringBootCondition { private void collectProperties(PropertyResolver resolver, List missing, List nonMatching) { - if (this.relaxedNames) { - resolver = new RelaxedPropertyResolver(resolver, this.prefix); - } for (String name : this.names) { - String key = (this.relaxedNames ? name : this.prefix + name); + String key = this.prefix + name; if (resolver.containsProperty(key)) { if (!isMatch(resolver.getProperty(key), this.havingValue)) { nonMatching.add(name); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java index 13ae5bb448..d0c3d4344d 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -26,10 +26,14 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.test.util.EnvironmentTestUtils; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -48,7 +52,9 @@ public class ConditionalOnPropertyTests { @Rule public ExpectedException thrown = ExpectedException.none(); - private AnnotationConfigApplicationContext context; + private ConfigurableApplicationContext context; + + private ConfigurableEnvironment environment = new StandardEnvironment(); @After public void tearDown() { @@ -98,13 +104,6 @@ public class ConditionalOnPropertyTests { assertThat(this.context.containsBean("foo")).isTrue(); } - @Test - public void nonRelaxedName() throws Exception { - load(NonRelaxedPropertiesRequiredConfiguration.class, - "theRelaxedProperty=value1"); - assertThat(this.context.containsBean("foo")).isFalse(); - } - @Test // Enabled by default public void enabledIfNotConfiguredOtherwise() { @@ -185,18 +184,6 @@ public class ConditionalOnPropertyTests { assertThat(this.context.containsBean("foo")).isTrue(); } - @Test - public void strictNameMatch() { - load(StrictNameConfig.class, "simple.my-property:bar"); - assertThat(this.context.containsBean("foo")).isTrue(); - } - - @Test - public void strictNameNoMatch() { - load(StrictNameConfig.class, "simple.myProperty:bar"); - assertThat(this.context.containsBean("foo")).isFalse(); - } - @Test public void multiValuesAllSet() { load(MultiValuesConfig.class, "simple.my-property:bar", @@ -271,10 +258,9 @@ public class ConditionalOnPropertyTests { } private void load(Class config, String... environment) { - this.context = new AnnotationConfigApplicationContext(); - EnvironmentTestUtils.addEnvironment(this.context, environment); - this.context.register(config); - this.context.refresh(); + EnvironmentTestUtils.addEnvironment(this.environment, environment); + this.context = new SpringApplicationBuilder(config).environment(this.environment) + .web(WebApplicationType.NONE).run(); } @Configuration @@ -310,17 +296,6 @@ public class ConditionalOnPropertyTests { } - @Configuration - @ConditionalOnProperty(name = "the-relaxed-property", relaxedNames = false) - protected static class NonRelaxedPropertiesRequiredConfiguration { - - @Bean - public String foo() { - return "foo"; - } - - } - @Configuration // i.e ${simple.myProperty:true} @ConditionalOnProperty(prefix = "simple", name = "my-property", havingValue = "true", matchIfMissing = true) @@ -378,17 +353,6 @@ public class ConditionalOnPropertyTests { } - @Configuration - @ConditionalOnProperty(prefix = "simple", name = "my-property", havingValue = "bar", relaxedNames = false) - static class StrictNameConfig { - - @Bean - public String foo() { - return "foo"; - } - - } - @Configuration @ConditionalOnProperty(prefix = "simple", name = { "my-property", "my-another-property" }, havingValue = "bar") @@ -434,6 +398,7 @@ public class ConditionalOnPropertyTests { } + @Configuration @ConditionalOnMyFeature protected static class MetaAnnotation { @@ -444,6 +409,7 @@ public class ConditionalOnPropertyTests { } + @Configuration @ConditionalOnMyFeature @ConditionalOnProperty(prefix = "my.other.feature", name = "enabled", havingValue = "true", matchIfMissing = false) protected static class MetaAnnotationAndDirectAnnotation { From 2e83de58ecb9a45bea3760d7b15145fc6f0f4231 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 25 Apr 2017 13:04:55 -0700 Subject: [PATCH 15/19] Test placeholder expansion supports relaxed names A test to ensure that the updated configuration properties support correctly expands placeholders using mapped properties. Prior to the updated support an `application.properties` file of the form: my.prefix.some-url=http://github.com my.prefix.login-url=${my.prefix.some-url}/login Would not correctly expand the placeholder against the environment variable `MY_PREFIX_SOME_URL` Closes gh-2496 --- ...onPropertiesBindingPostProcessorTests.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java index 18838f6c06..8d6ceaf514 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.context.properties; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,6 +43,10 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.SystemEnvironmentPropertySource; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.test.util.ReflectionTestUtils; @@ -362,6 +368,39 @@ public class ConfigurationPropertiesBindingPostProcessorTests { .containsEntry("foo", "bar"); } + @Test + public void overridingPropertiesInEnvShouldOverride() throws Exception { + this.context = new AnnotationConfigApplicationContext(); + ConfigurableEnvironment env = this.context.getEnvironment(); + MutablePropertySources propertySources = env.getPropertySources(); + propertySources.addFirst(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("COM_EXAMPLE_FOO", "10"))); + propertySources.addLast(new MapPropertySource("test", + Collections.singletonMap("com.example.foo", 5))); + this.context.register(TestConfiguration.class); + this.context.refresh(); + int foo = this.context.getBean(TestConfiguration.class).getFoo(); + assertThat(foo).isEqualTo(10); + } + + @Test + public void overridingPropertiesWithPlaceholderResolutionInEnvShouldOverride() + throws Exception { + this.context = new AnnotationConfigApplicationContext(); + ConfigurableEnvironment env = this.context.getEnvironment(); + MutablePropertySources propertySources = env.getPropertySources(); + propertySources.addFirst(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("COM_EXAMPLE_BAR", "10"))); + Map source = new HashMap<>(); + source.put("com.example.bar", 5); + source.put("com.example.foo", "${com.example.bar}"); + propertySources.addLast(new MapPropertySource("test", source)); + this.context.register(TestConfiguration.class); + this.context.refresh(); + int foo = this.context.getBean(TestConfiguration.class).getFoo(); + assertThat(foo).isEqualTo(10); + } + private void assertBindingFailure(int errorCount) { try { this.context.refresh(); From e3ea7a7d938faeebba7c88847ce51bb64c21733f Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 25 Apr 2017 13:05:08 -0700 Subject: [PATCH 16/19] Test binding of environment variables to maps Add a test case to show that the updated configuration properties binder correctly binds environment variables to complex maps. Closes gh-8902 --- ...onPropertiesBindingPostProcessorTests.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java index 8d6ceaf514..67a649e5a0 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java @@ -368,6 +368,22 @@ public class ConfigurationPropertiesBindingPostProcessorTests { .containsEntry("foo", "bar"); } + @Test + public void systemPropertiesShouldBindToMap() throws Exception { + MockEnvironment env = new MockEnvironment(); + MutablePropertySources propertySources = env.getPropertySources(); + propertySources.addLast(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("TEST_MAP_FOO_BAR", "baz"))); + this.context = new AnnotationConfigApplicationContext(); + this.context.setEnvironment(env); + this.context.register(PropertiesWithComplexMap.class); + this.context.refresh(); + Map> map = this.context + .getBean(PropertiesWithComplexMap.class).getMap(); + Map foo = map.get("foo"); + assertThat(foo).containsEntry("bar", "baz"); + } + @Test public void overridingPropertiesInEnvShouldOverride() throws Exception { this.context = new AnnotationConfigApplicationContext(); @@ -765,6 +781,23 @@ public class ConfigurationPropertiesBindingPostProcessorTests { } + @Configuration + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "test") + public static class PropertiesWithComplexMap { + + private Map> map; + + public Map> getMap() { + return this.map; + } + + public void setMap(Map> map) { + this.map = map; + } + + } + @Configuration @EnableConfigurationProperties public static class ConfigurationPropertiesWithFactoryBean { From 6184e4154e0683fe5fcce6efc0c7a67bc41a35ea Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Wed, 26 Apr 2017 12:21:21 -0700 Subject: [PATCH 17/19] Test correct binding in '/info' endpoint Add a test to ensure that the updated configuration properties work correctly binds environment sources in the `/info` endpoint. Closes gh-7388 --- .../info/EnvironmentInfoContributorTests.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/EnvironmentInfoContributorTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/EnvironmentInfoContributorTests.java index d1ced85b9b..c363d435ee 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/EnvironmentInfoContributorTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/EnvironmentInfoContributorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -16,11 +16,16 @@ package org.springframework.boot.actuate.info; +import java.util.Collections; +import java.util.Map; + import org.junit.Test; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -50,6 +55,16 @@ public class EnvironmentInfoContributorTests { assertThat(actual.getDetails().size()).isEqualTo(0); } + @Test + @SuppressWarnings("unchecked") + public void propertiesFromEnvironmentShouldBindCorrectly() throws Exception { + MutablePropertySources propertySources = this.environment.getPropertySources(); + propertySources.addFirst(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("INFO_ENVIRONMENT_FOO", "green"))); + Info actual = contributeFrom(this.environment); + assertThat(actual.get("environment", Map.class)).containsEntry("foo", "green"); + } + private static Info contributeFrom(ConfigurableEnvironment environment) { EnvironmentInfoContributor contributor = new EnvironmentInfoContributor( environment); From 5edb1194f57a3f2f102f4bffa2dc66d4cfa9dbf4 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Wed, 26 Apr 2017 12:21:39 -0700 Subject: [PATCH 18/19] Test binding of Map Add a test to ensure that the new binder can bind correctly to a Map. Closes gh-3789 --- .../properties/bind/MapBinderTests.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java index 6776686110..971d227446 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -25,6 +25,7 @@ import java.util.Map; import org.junit.Before; import org.junit.Test; import org.mockito.Answers; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.springframework.boot.context.properties.bind.BinderTests.ExampleEnum; @@ -64,6 +65,9 @@ public class MapBinderTests { private static final Bindable> STRING_OBJECT_MAP = Bindable .mapOf(String.class, Object.class); + private static final Bindable> STRING_ARRAY_MAP = Bindable + .mapOf(String.class, String[].class); + private List sources = new ArrayList<>(); private Binder binder; @@ -352,4 +356,21 @@ public class MapBinderTests { eq(target), any(), isA(Map.class)); } + @Test + public void bindToMapStringArrayShouldTriggerOnSuccess() throws Exception { + this.sources + .add(new MockConfigurationPropertySource("foo.bar", "a,b,c", "line1")); + BindHandler handler = mock(BindHandler.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + Bindable> target = STRING_ARRAY_MAP; + this.binder.bind("foo", target, handler); + InOrder inOrder = inOrder(handler); + ArgumentCaptor array = ArgumentCaptor.forClass(String[].class); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo.bar")), + eq(Bindable.of(String[].class)), any(), array.capture()); + assertThat(array.getValue()).containsExactly("a", "b", "c"); + inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), + eq(target), any(), isA(Map.class)); + } + } From 4e3d280378791e91d91720537cf3279f5f48b37c Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 25 Apr 2017 13:29:15 -0700 Subject: [PATCH 19/19] Delete deprecate relaxed binder code Remove the deprecated relaxed binder code that has now been replaced with the configuration properties binder. Closes gh-9000 --- .../DefaultPropertyNamePatternsMatcher.java | 119 ------ .../bind/PropertiesConfigurationFactory.java | 346 ------------------ .../boot/bind/PropertySourceUtils.java | 92 ----- .../boot/bind/PropertySourcesBinder.java | 129 ------- .../boot/bind/RelaxedNames.java | 1 - .../boot/bind/RelaxedPropertyResolver.java | 156 -------- ...faultPropertyNamePatternsMatcherTests.java | 83 ----- ...ropertiesConfigurationFactoryMapTests.java | 133 ------- ...onfigurationFactoryParameterizedTests.java | 162 -------- ...sConfigurationFactoryPerformanceTests.java | 111 ------ .../PropertiesConfigurationFactoryTests.java | 291 --------------- .../boot/bind/PropertySourcesBinderTests.java | 99 ----- .../bind/PropertySourcesBindingTests.java | 147 -------- .../PropertySourcesPropertyValuesTests.java | 312 ---------------- .../boot/bind/RelaxedNamesTests.java | 150 -------- .../bind/RelaxedPropertyResolverTests.java | 197 ---------- .../SimplerPropertySourcesBindingTests.java | 87 ----- 17 files changed, 2615 deletions(-) delete mode 100644 spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java delete mode 100644 spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java delete mode 100644 spring-boot/src/main/java/org/springframework/boot/bind/PropertySourceUtils.java delete mode 100644 spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesBinder.java delete mode 100644 spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcherTests.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryMapTests.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryParameterizedTests.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryPerformanceTests.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryTests.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesBinderTests.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesBindingTests.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesPropertyValuesTests.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/RelaxedNamesTests.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/RelaxedPropertyResolverTests.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/SimplerPropertySourcesBindingTests.java diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java b/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java deleted file mode 100644 index 66cab537c8..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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.bind; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * {@link PropertyNamePatternsMatcher} that matches when a property name exactly matches - * one of the given names, or starts with one of the given names followed by a delimiter. - * This implementation is optimized for frequent calls. - * - * @author Phillip Webb - * @since 1.2.0 - */ -@Deprecated -class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher { - - private final char[] delimiters; - - private final boolean ignoreCase; - - private final String[] names; - - protected DefaultPropertyNamePatternsMatcher(char[] delimiters, String... names) { - this(delimiters, false, names); - } - - protected DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase, - String... names) { - this(delimiters, ignoreCase, new HashSet<>(Arrays.asList(names))); - } - - DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase, - Set names) { - this.delimiters = delimiters; - this.ignoreCase = ignoreCase; - this.names = names.toArray(new String[names.size()]); - } - - @Override - public boolean matches(String propertyName) { - char[] propertyNameChars = propertyName.toCharArray(); - boolean[] match = new boolean[this.names.length]; - boolean noneMatched = true; - for (int i = 0; i < this.names.length; i++) { - if (this.names[i].length() <= propertyNameChars.length) { - match[i] = true; - noneMatched = false; - } - } - if (noneMatched) { - return false; - } - for (int charIndex = 0; charIndex < propertyNameChars.length; charIndex++) { - for (int nameIndex = 0; nameIndex < this.names.length; nameIndex++) { - if (match[nameIndex]) { - match[nameIndex] = false; - if (charIndex < this.names[nameIndex].length()) { - if (isCharMatch(this.names[nameIndex].charAt(charIndex), - propertyNameChars[charIndex])) { - match[nameIndex] = true; - noneMatched = false; - } - } - else { - char charAfter = propertyNameChars[this.names[nameIndex] - .length()]; - if (isDelimiter(charAfter)) { - match[nameIndex] = true; - noneMatched = false; - } - } - } - } - if (noneMatched) { - return false; - } - } - for (int i = 0; i < match.length; i++) { - if (match[i]) { - return true; - } - } - return false; - } - - private boolean isCharMatch(char c1, char c2) { - if (this.ignoreCase) { - return Character.toLowerCase(c1) == Character.toLowerCase(c2); - } - return c1 == c2; - } - - private boolean isDelimiter(char c) { - for (char delimiter : this.delimiters) { - if (c == delimiter) { - return true; - } - } - return false; - } - -} diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java deleted file mode 100644 index 27bf28d6e0..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * 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.bind; - -import java.beans.PropertyDescriptor; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.Set; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.BeanUtils; -import org.springframework.beans.PropertyValues; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.MessageSource; -import org.springframework.context.MessageSourceAware; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.env.PropertySources; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.validation.BindException; -import org.springframework.validation.BindingResult; -import org.springframework.validation.DataBinder; -import org.springframework.validation.ObjectError; -import org.springframework.validation.Validator; - -/** - * Validate some {@link Properties} (or optionally {@link PropertySources}) by binding - * them to an object of a specified type and then optionally running a {@link Validator} - * over it. - * - * @param the target type - * @author Dave Syer - */ -@Deprecated -public class PropertiesConfigurationFactory - implements FactoryBean, MessageSourceAware, InitializingBean { - - private static final char[] EXACT_DELIMITERS = { '_', '.', '[' }; - - private static final char[] TARGET_NAME_DELIMITERS = { '_', '.' }; - - private static final Log logger = LogFactory - .getLog(PropertiesConfigurationFactory.class); - - private boolean ignoreUnknownFields = true; - - private boolean ignoreInvalidFields; - - private PropertySources propertySources; - - private final T target; - - private Validator validator; - - private MessageSource messageSource; - - private boolean hasBeenBound = false; - - private boolean ignoreNestedProperties = false; - - private String targetName; - - private ConversionService conversionService; - - private boolean resolvePlaceholders = true; - - /** - * Create a new {@link PropertiesConfigurationFactory} instance. - * @param target the target object to bind too - * @see #PropertiesConfigurationFactory(Class) - */ - public PropertiesConfigurationFactory(T target) { - Assert.notNull(target, "Target object must not be null"); - this.target = target; - } - - /** - * Create a new {@link PropertiesConfigurationFactory} instance. - * @param type the target type - * @see #PropertiesConfigurationFactory(Class) - */ - @SuppressWarnings("unchecked") - public PropertiesConfigurationFactory(Class type) { - Assert.notNull(type, "Target type must not be null"); - this.target = (T) BeanUtils.instantiateClass(type); - } - - /** - * Flag to disable binding of nested properties (i.e. those with period separators in - * their paths). Can be useful to disable this if the name prefix is empty and you - * don't want to ignore unknown fields. - * @param ignoreNestedProperties the flag to set (default false) - */ - public void setIgnoreNestedProperties(boolean ignoreNestedProperties) { - this.ignoreNestedProperties = ignoreNestedProperties; - } - - /** - * Set whether to ignore unknown fields, that is, whether to ignore bind parameters - * that do not have corresponding fields in the target object. - *

- * Default is "true". Turn this off to enforce that all bind parameters must have a - * matching field in the target object. - * @param ignoreUnknownFields if unknown fields should be ignored - */ - public void setIgnoreUnknownFields(boolean ignoreUnknownFields) { - this.ignoreUnknownFields = ignoreUnknownFields; - } - - /** - * Set whether to ignore invalid fields, that is, whether to ignore bind parameters - * that have corresponding fields in the target object which are not accessible (for - * example because of null values in the nested path). - *

- * Default is "false". Turn this on to ignore bind parameters for nested objects in - * non-existing parts of the target object graph. - * @param ignoreInvalidFields if invalid fields should be ignored - */ - public void setIgnoreInvalidFields(boolean ignoreInvalidFields) { - this.ignoreInvalidFields = ignoreInvalidFields; - } - - /** - * Set the target name. - * @param targetName the target name - */ - public void setTargetName(String targetName) { - this.targetName = targetName; - } - - /** - * Set the message source. - * @param messageSource the message source - */ - @Override - public void setMessageSource(MessageSource messageSource) { - this.messageSource = messageSource; - } - - /** - * Set the property sources. - * @param propertySources the property sources - */ - public void setPropertySources(PropertySources propertySources) { - this.propertySources = propertySources; - } - - /** - * Set the conversion service. - * @param conversionService the conversion service - */ - public void setConversionService(ConversionService conversionService) { - this.conversionService = conversionService; - } - - /** - * Set the validator. - * @param validator the validator - */ - public void setValidator(Validator validator) { - this.validator = validator; - } - - /** - * Flag to indicate that placeholders should be replaced during binding. Default is - * true. - * @param resolvePlaceholders flag value - */ - public void setResolvePlaceholders(boolean resolvePlaceholders) { - this.resolvePlaceholders = resolvePlaceholders; - } - - @Override - public void afterPropertiesSet() throws Exception { - bindPropertiesToTarget(); - } - - @Override - public Class getObjectType() { - if (this.target == null) { - return Object.class; - } - return this.target.getClass(); - } - - @Override - public boolean isSingleton() { - return true; - } - - @Override - public T getObject() throws Exception { - if (!this.hasBeenBound) { - bindPropertiesToTarget(); - } - return this.target; - } - - public void bindPropertiesToTarget() throws BindException { - Assert.state(this.propertySources != null, "PropertySources should not be null"); - if (logger.isTraceEnabled()) { - logger.trace("Property Sources: " + this.propertySources); - - } - this.hasBeenBound = true; - doBindPropertiesToTarget(); - } - - private void doBindPropertiesToTarget() throws BindException { - RelaxedDataBinder dataBinder = (this.targetName != null - ? new RelaxedDataBinder(this.target, this.targetName) - : new RelaxedDataBinder(this.target)); - if (this.validator != null - && this.validator.supports(dataBinder.getTarget().getClass())) { - dataBinder.setValidator(this.validator); - } - if (this.conversionService != null) { - dataBinder.setConversionService(this.conversionService); - } - dataBinder.setAutoGrowCollectionLimit(Integer.MAX_VALUE); - dataBinder.setIgnoreNestedProperties(this.ignoreNestedProperties); - dataBinder.setIgnoreInvalidFields(this.ignoreInvalidFields); - dataBinder.setIgnoreUnknownFields(this.ignoreUnknownFields); - customizeBinder(dataBinder); - Iterable relaxedTargetNames = getRelaxedTargetNames(); - Set names = getNames(relaxedTargetNames); - PropertyValues propertyValues = getPropertySourcesPropertyValues(names, - relaxedTargetNames); - dataBinder.bind(propertyValues); - if (this.validator != null) { - dataBinder.validate(); - } - checkForBindingErrors(dataBinder); - } - - private Iterable getRelaxedTargetNames() { - return (this.target != null && StringUtils.hasLength(this.targetName) - ? new RelaxedNames(this.targetName) : null); - } - - private Set getNames(Iterable prefixes) { - Set names = new LinkedHashSet<>(); - if (this.target != null) { - PropertyDescriptor[] descriptors = BeanUtils - .getPropertyDescriptors(this.target.getClass()); - for (PropertyDescriptor descriptor : descriptors) { - String name = descriptor.getName(); - if (!name.equals("class")) { - RelaxedNames relaxedNames = RelaxedNames.forCamelCase(name); - if (prefixes == null) { - for (String relaxedName : relaxedNames) { - names.add(relaxedName); - } - } - else { - for (String prefix : prefixes) { - for (String relaxedName : relaxedNames) { - names.add(prefix + "." + relaxedName); - names.add(prefix + "_" + relaxedName); - } - } - } - } - } - } - return names; - } - - private PropertyValues getPropertySourcesPropertyValues(Set names, - Iterable relaxedTargetNames) { - PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names, - relaxedTargetNames); - return new PropertySourcesPropertyValues(this.propertySources, names, includes, - this.resolvePlaceholders); - } - - private PropertyNamePatternsMatcher getPropertyNamePatternsMatcher(Set names, - Iterable relaxedTargetNames) { - if (this.ignoreUnknownFields && !isMapTarget()) { - // Since unknown fields are ignored we can filter them out early to save - // unnecessary calls to the PropertySource. - return new DefaultPropertyNamePatternsMatcher(EXACT_DELIMITERS, true, names); - } - if (relaxedTargetNames != null) { - // We can filter properties to those starting with the target name, but - // we can't do a complete filter since we need to trigger the - // unknown fields check - Set relaxedNames = new HashSet<>(); - for (String relaxedTargetName : relaxedTargetNames) { - relaxedNames.add(relaxedTargetName); - } - return new DefaultPropertyNamePatternsMatcher(TARGET_NAME_DELIMITERS, true, - relaxedNames); - } - // Not ideal, we basically can't filter anything - return PropertyNamePatternsMatcher.ALL; - } - - private boolean isMapTarget() { - return this.target != null && Map.class.isAssignableFrom(this.target.getClass()); - } - - private void checkForBindingErrors(RelaxedDataBinder dataBinder) - throws BindException { - BindingResult errors = dataBinder.getBindingResult(); - if (errors.hasErrors()) { - logger.error("Properties configuration failed validation"); - for (ObjectError error : errors.getAllErrors()) { - logger.error( - this.messageSource != null - ? this.messageSource.getMessage(error, - Locale.getDefault()) + " (" + error + ")" - : error); - } - throw new BindException(errors); - } - } - - /** - * Customize the data binder. - * @param dataBinder the data binder that will be used to bind and validate - */ - protected void customizeBinder(DataBinder dataBinder) { - } - -} diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourceUtils.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourceUtils.java deleted file mode 100644 index d9dd190954..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourceUtils.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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.bind; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.core.env.EnumerablePropertySource; -import org.springframework.core.env.PropertySource; -import org.springframework.core.env.PropertySources; - -/** - * Convenience class for manipulating PropertySources. - * - * @author Dave Syer - * @see PropertySource - * @see PropertySources - */ -@Deprecated -public abstract class PropertySourceUtils { - - /** - * Return a Map of all values from the specified {@link PropertySources} that start - * with a particular key. - * @param propertySources the property sources to scan - * @param keyPrefix the key prefixes to test - * @return a map of all sub properties starting with the specified key prefixes. - * @see PropertySourceUtils#getSubProperties(PropertySources, String, String) - */ - public static Map getSubProperties(PropertySources propertySources, - String keyPrefix) { - return PropertySourceUtils.getSubProperties(propertySources, null, keyPrefix); - } - - /** - * Return a Map of all values from the specified {@link PropertySources} that start - * with a particular key. - * @param propertySources the property sources to scan - * @param rootPrefix a root prefix to be prepended to the keyPrefix (can be - * {@code null}) - * @param keyPrefix the key prefixes to test - * @return a map of all sub properties starting with the specified key prefixes. - * @see #getSubProperties(PropertySources, String, String) - */ - public static Map getSubProperties(PropertySources propertySources, - String rootPrefix, String keyPrefix) { - RelaxedNames keyPrefixes = new RelaxedNames(keyPrefix); - Map subProperties = new LinkedHashMap<>(); - for (PropertySource source : propertySources) { - if (source instanceof EnumerablePropertySource) { - for (String name : ((EnumerablePropertySource) source) - .getPropertyNames()) { - String key = PropertySourceUtils.getSubKey(name, rootPrefix, - keyPrefixes); - if (key != null && !subProperties.containsKey(key)) { - subProperties.put(key, source.getProperty(name)); - } - } - } - } - return Collections.unmodifiableMap(subProperties); - } - - private static String getSubKey(String name, String rootPrefixes, - RelaxedNames keyPrefix) { - rootPrefixes = (rootPrefixes == null ? "" : rootPrefixes); - for (String rootPrefix : new RelaxedNames(rootPrefixes)) { - for (String candidateKeyPrefix : keyPrefix) { - if (name.startsWith(rootPrefix + candidateKeyPrefix)) { - return name.substring((rootPrefix + candidateKeyPrefix).length()); - } - } - } - return null; - } - -} diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesBinder.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesBinder.java deleted file mode 100644 index c775014693..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesBinder.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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.bind; - -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.core.convert.ConversionService; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertySource; -import org.springframework.core.env.PropertySources; -import org.springframework.util.StringUtils; -import org.springframework.validation.BindException; - -/** - * Helper extracting info from {@link PropertySources}. - * - * @author Stephane Nicoll - * @since 1.4.0 - */ -@Deprecated -public class PropertySourcesBinder { - - private PropertySources propertySources; - - private ConversionService conversionService; - - /** - * Create a new instance. - * @param propertySources the {@link PropertySources} to use - */ - public PropertySourcesBinder(PropertySources propertySources) { - this.propertySources = propertySources; - } - - /** - * Create a new instance from a single {@link PropertySource}. - * @param propertySource the {@link PropertySource} to use - */ - public PropertySourcesBinder(PropertySource propertySource) { - this(createPropertySources(propertySource)); - } - - /** - * Create a new instance using the {@link Environment} as the property sources. - * @param environment the environment - */ - public PropertySourcesBinder(ConfigurableEnvironment environment) { - this(environment.getPropertySources()); - } - - public void setPropertySources(PropertySources propertySources) { - this.propertySources = propertySources; - } - - public PropertySources getPropertySources() { - return this.propertySources; - } - - public void setConversionService(ConversionService conversionService) { - this.conversionService = conversionService; - } - - public ConversionService getConversionService() { - return this.conversionService; - } - - /** - * Extract the keys using the specified {@code prefix}. The prefix won't be included. - *

- * Any key that starts with the {@code prefix} will be included. - * @param prefix the prefix to use - * @return the keys matching the prefix - */ - public Map extractAll(String prefix) { - Map content = new LinkedHashMap<>(); - bindTo(prefix, content); - return content; - } - - /** - * Bind the specified {@code target} from the environment using the {@code prefix}. - *

- * Any key that starts with the {@code prefix} will be bound to the {@code target}. - * @param prefix the prefix to use - * @param target the object to bind to - */ - public void bindTo(String prefix, Object target) { - PropertiesConfigurationFactory factory = new PropertiesConfigurationFactory<>( - target); - if (StringUtils.hasText(prefix)) { - factory.setTargetName(prefix); - } - if (this.conversionService != null) { - factory.setConversionService(this.conversionService); - } - factory.setPropertySources(this.propertySources); - try { - factory.bindPropertiesToTarget(); - } - catch (BindException ex) { - throw new IllegalStateException("Cannot bind to " + target, ex); - } - } - - private static PropertySources createPropertySources( - PropertySource propertySource) { - MutablePropertySources propertySources = new MutablePropertySources(); - propertySources.addLast(propertySource); - return propertySources; - } - -} diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedNames.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedNames.java index 4b54935866..63c82fa04b 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedNames.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedNames.java @@ -30,7 +30,6 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Dave Syer * @see RelaxedDataBinder - * @see RelaxedPropertyResolver */ @Deprecated public final class RelaxedNames implements Iterable { diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java deleted file mode 100644 index 93ab74dece..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.bind; - -import java.util.Map; - -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.core.env.PropertyResolver; -import org.springframework.core.env.PropertySourcesPropertyResolver; -import org.springframework.util.Assert; - -/** - * {@link PropertyResolver} that attempts to resolve values using {@link RelaxedNames}. - * - * @author Phillip Webb - * @see RelaxedNames - */ -@Deprecated -public class RelaxedPropertyResolver implements PropertyResolver { - - private final PropertyResolver resolver; - - private final String prefix; - - public RelaxedPropertyResolver(PropertyResolver resolver) { - this(resolver, null); - } - - public RelaxedPropertyResolver(PropertyResolver resolver, String prefix) { - Assert.notNull(resolver, "PropertyResolver must not be null"); - this.resolver = resolver; - this.prefix = (prefix == null ? "" : prefix); - } - - @Override - public String getRequiredProperty(String key) throws IllegalStateException { - return getRequiredProperty(key, String.class); - } - - @Override - public T getRequiredProperty(String key, Class targetType) - throws IllegalStateException { - T value = getProperty(key, targetType); - Assert.state(value != null, String.format("required key [%s] not found", key)); - return value; - } - - @Override - public String getProperty(String key) { - return getProperty(key, String.class, null); - } - - @Override - public String getProperty(String key, String defaultValue) { - return getProperty(key, String.class, defaultValue); - } - - @Override - public T getProperty(String key, Class targetType) { - return getProperty(key, targetType, null); - } - - @Override - public T getProperty(String key, Class targetType, T defaultValue) { - RelaxedNames prefixes = new RelaxedNames(this.prefix); - RelaxedNames keys = new RelaxedNames(key); - for (String prefix : prefixes) { - for (String relaxedKey : keys) { - if (this.resolver.containsProperty(prefix + relaxedKey)) { - return this.resolver.getProperty(prefix + relaxedKey, targetType); - } - } - } - return defaultValue; - } - - @Override - public boolean containsProperty(String key) { - RelaxedNames prefixes = new RelaxedNames(this.prefix); - RelaxedNames keys = new RelaxedNames(key); - for (String prefix : prefixes) { - for (String relaxedKey : keys) { - if (this.resolver.containsProperty(prefix + relaxedKey)) { - return true; - } - } - } - return false; - } - - @Override - public String resolvePlaceholders(String text) { - throw new UnsupportedOperationException( - "Unable to resolve placeholders with relaxed properties"); - } - - @Override - public String resolveRequiredPlaceholders(String text) - throws IllegalArgumentException { - throw new UnsupportedOperationException( - "Unable to resolve placeholders with relaxed properties"); - } - - /** - * Return a Map of all values from all underlying properties that start with the - * specified key. NOTE: this method can only be used if the underlying resolver is a - * {@link ConfigurableEnvironment}. - * @param keyPrefix the key prefix used to filter results - * @return a map of all sub properties starting with the specified key prefix. - * @see PropertySourceUtils#getSubProperties - */ - public Map getSubProperties(String keyPrefix) { - Assert.isInstanceOf(ConfigurableEnvironment.class, this.resolver, - "SubProperties not available."); - ConfigurableEnvironment env = (ConfigurableEnvironment) this.resolver; - return PropertySourceUtils.getSubProperties(env.getPropertySources(), this.prefix, - keyPrefix); - } - - /** - * Return a property resolver for the environment, preferring one that ignores - * unresolvable nested placeholders. - * @param environment the source environment - * @param prefix the prefix - * @return a property resolver for the environment - * @since 1.4.3 - */ - public static RelaxedPropertyResolver ignoringUnresolvableNestedPlaceholders( - Environment environment, String prefix) { - Assert.notNull(environment, "Environment must not be null"); - PropertyResolver resolver = environment; - if (environment instanceof ConfigurableEnvironment) { - resolver = new PropertySourcesPropertyResolver( - ((ConfigurableEnvironment) environment).getPropertySources()); - ((PropertySourcesPropertyResolver) resolver) - .setIgnoreUnresolvableNestedPlaceholders(true); - } - return new RelaxedPropertyResolver(resolver, prefix); - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcherTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcherTests.java deleted file mode 100644 index 699c98248e..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcherTests.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2012-2016 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.bind; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link DefaultPropertyNamePatternsMatcher}. - * - * @author Phillip Webb - */ -public class DefaultPropertyNamePatternsMatcherTests { - - private static final char[] DELIMITERS = { '.', '_' }; - - @Test - public void namesShorter() { - assertThat(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb") - .matches("zzzzz")).isFalse(); - - } - - @Test - public void namesExactMatch() { - assertThat( - new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb", "cccc") - .matches("bbbb")).isTrue(); - } - - @Test - public void namesLonger() { - assertThat(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaaa", "bbbbb", - "ccccc").matches("bbbb")).isFalse(); - } - - @Test - public void nameWithDot() throws Exception { - assertThat( - new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb", "cccc") - .matches("bbbb.anything")).isTrue(); - } - - @Test - public void nameWithUnderscore() throws Exception { - assertThat( - new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb", "cccc") - .matches("bbbb_anything")).isTrue(); - } - - @Test - public void namesMatchWithDifferentLengths() throws Exception { - assertThat( - new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaa", "bbbb", "ccccc") - .matches("bbbb")).isTrue(); - } - - @Test - public void withSquareBrackets() throws Exception { - char[] delimiters = "._[".toCharArray(); - PropertyNamePatternsMatcher matcher = new DefaultPropertyNamePatternsMatcher( - delimiters, "aaa", "bbbb", "ccccc"); - assertThat(matcher.matches("bbbb")).isTrue(); - assertThat(matcher.matches("bbbb[4]")).isTrue(); - assertThat(matcher.matches("bbb[4]")).isFalse(); - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryMapTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryMapTests.java deleted file mode 100644 index 03e661ba8f..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryMapTests.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.bind; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; - -import org.junit.Test; - -import org.springframework.context.support.StaticMessageSource; -import org.springframework.core.env.CompositePropertySource; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertiesPropertySource; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.support.PropertiesLoaderUtils; -import org.springframework.validation.Validator; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PropertiesConfigurationFactory} binding to a map. - * - * @author Dave Syer - */ -public class PropertiesConfigurationFactoryMapTests { - - private PropertiesConfigurationFactory factory; - - private Validator validator; - - private boolean ignoreUnknownFields = true; - - private String targetName = null; - - @Test - public void testValidPropertiesLoadsWithNoErrors() throws Exception { - Foo foo = createFoo("map.name: blah\nmap.bar: blah"); - assertThat(foo.map.get("bar")).isEqualTo("blah"); - assertThat(foo.map.get("name")).isEqualTo("blah"); - } - - @Test - public void testBindToNamedTarget() throws Exception { - this.targetName = "foo"; - Foo foo = createFoo("hi: hello\nfoo.map.name: foo\nfoo.map.bar: blah"); - assertThat(foo.map.get("bar")).isEqualTo("blah"); - } - - @Test - public void testBindFromPropertySource() throws Exception { - this.targetName = "foo"; - setupFactory(); - MutablePropertySources sources = new MutablePropertySources(); - sources.addFirst(new MapPropertySource("map", - Collections.singletonMap("foo.map.name", (Object) "blah"))); - this.factory.setPropertySources(sources); - this.factory.afterPropertiesSet(); - Foo foo = this.factory.getObject(); - assertThat(foo.map.get("name")).isEqualTo("blah"); - } - - @Test - public void testBindFromCompositePropertySource() throws Exception { - this.targetName = "foo"; - setupFactory(); - MutablePropertySources sources = new MutablePropertySources(); - CompositePropertySource composite = new CompositePropertySource("composite"); - composite.addPropertySource(new MapPropertySource("map", - Collections.singletonMap("foo.map.name", (Object) "blah"))); - sources.addFirst(composite); - this.factory.setPropertySources(sources); - this.factory.afterPropertiesSet(); - Foo foo = this.factory.getObject(); - assertThat(foo.map.get("name")).isEqualTo("blah"); - } - - private Foo createFoo(final String values) throws Exception { - setupFactory(); - return bindFoo(values); - } - - private Foo bindFoo(final String values) throws Exception { - Properties properties = PropertiesLoaderUtils - .loadProperties(new ByteArrayResource(values.getBytes())); - MutablePropertySources propertySources = new MutablePropertySources(); - propertySources.addFirst(new PropertiesPropertySource("test", properties)); - this.factory.setPropertySources(propertySources); - this.factory.afterPropertiesSet(); - return this.factory.getObject(); - } - - private void setupFactory() throws IOException { - this.factory = new PropertiesConfigurationFactory<>(Foo.class); - this.factory.setValidator(this.validator); - this.factory.setTargetName(this.targetName); - this.factory.setIgnoreUnknownFields(this.ignoreUnknownFields); - this.factory.setMessageSource(new StaticMessageSource()); - } - - // Foo needs to be public and to have setters for all properties - public static class Foo { - - private Map map = new HashMap<>(); - - public Map getMap() { - return this.map; - } - - public void setMap(Map map) { - this.map = map; - } - - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryParameterizedTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryParameterizedTests.java deleted file mode 100644 index 6f6645918d..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryParameterizedTests.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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.bind; - -import java.io.IOException; -import java.util.Properties; - -import javax.validation.constraints.NotNull; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -import org.springframework.context.support.StaticMessageSource; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertiesPropertySource; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.support.PropertiesLoaderUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Parameterized tests for {@link PropertiesConfigurationFactory} - * - * @author Dave Syer - * @author Andy Wilkinson - */ -@RunWith(Parameterized.class) -public class PropertiesConfigurationFactoryParameterizedTests { - - private String targetName; - - private PropertiesConfigurationFactory factory = new PropertiesConfigurationFactory<>( - Foo.class); - - @Parameters - public static Object[] parameters() { - return new Object[] { new Object[] { false }, new Object[] { true } }; - } - - public PropertiesConfigurationFactoryParameterizedTests(boolean ignoreUnknownFields) { - this.factory.setIgnoreUnknownFields(ignoreUnknownFields); - } - - @Test - public void testValidPropertiesLoadsWithNoErrors() throws Exception { - Foo foo = createFoo("name: blah\nbar: blah"); - assertThat(foo.bar).isEqualTo("blah"); - assertThat(foo.name).isEqualTo("blah"); - } - - @Test - public void testValidPropertiesLoadsWithUpperCase() throws Exception { - Foo foo = createFoo("NAME: blah\nbar: blah"); - assertThat(foo.bar).isEqualTo("blah"); - assertThat(foo.name).isEqualTo("blah"); - } - - @Test - public void testUnderscore() throws Exception { - Foo foo = createFoo("spring_foo_baz: blah\nname: blah"); - assertThat(foo.spring_foo_baz).isEqualTo("blah"); - assertThat(foo.name).isEqualTo("blah"); - } - - @Test - public void testBindToNamedTarget() throws Exception { - this.targetName = "foo"; - Foo foo = createFoo("hi: hello\nfoo.name: foo\nfoo.bar: blah"); - assertThat(foo.bar).isEqualTo("blah"); - } - - @Test - public void testBindToNamedTargetUppercaseUnderscores() throws Exception { - this.targetName = "foo"; - Foo foo = createFoo("FOO_NAME: foo\nFOO_BAR: blah"); - assertThat(foo.bar).isEqualTo("blah"); - } - - private Foo createFoo(final String values) throws Exception { - setupFactory(); - return bindFoo(values); - } - - private Foo bindFoo(final String values) throws Exception { - Properties properties = PropertiesLoaderUtils - .loadProperties(new ByteArrayResource(values.getBytes())); - MutablePropertySources propertySources = new MutablePropertySources(); - propertySources.addFirst(new PropertiesPropertySource("test", properties)); - this.factory.setPropertySources(propertySources); - - this.factory.afterPropertiesSet(); - return this.factory.getObject(); - } - - private void setupFactory() throws IOException { - this.factory.setTargetName(this.targetName); - this.factory.setMessageSource(new StaticMessageSource()); - } - - // Foo needs to be public and to have setters for all properties - public static class Foo { - - @NotNull - private String name; - - private String bar; - - private String spring_foo_baz; - - private String fooBar; - - public String getSpringFooBaz() { - return this.spring_foo_baz; - } - - public void setSpringFooBaz(String spring_foo_baz) { - this.spring_foo_baz = spring_foo_baz; - } - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public String getBar() { - return this.bar; - } - - public void setBar(String bar) { - this.bar = bar; - } - - public String getFooBar() { - return this.fooBar; - } - - public void setFooBar(String fooBar) { - this.fooBar = fooBar; - } - - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryPerformanceTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryPerformanceTests.java deleted file mode 100644 index 73dfb1b76b..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryPerformanceTests.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.bind; - -import java.io.IOException; - -import javax.validation.constraints.NotNull; - -import org.junit.BeforeClass; -import org.junit.experimental.theories.DataPoints; -import org.junit.experimental.theories.Theories; -import org.junit.experimental.theories.Theory; -import org.junit.runner.RunWith; - -import org.springframework.context.support.StaticMessageSource; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.test.context.support.TestPropertySourceUtils; -import org.springframework.validation.Validator; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Performance tests for {@link PropertiesConfigurationFactory}. - * - * @author Dave Syer - */ -@RunWith(Theories.class) -public class PropertiesConfigurationFactoryPerformanceTests { - - @DataPoints - public static String[] values = new String[1000]; - - private PropertiesConfigurationFactory factory; - - private Validator validator; - - private boolean ignoreUnknownFields = true; - - private String targetName = null; - - private static StandardEnvironment environment = new StandardEnvironment(); - - @BeforeClass - public static void init() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, - "name=blah", "bar=blah"); - } - - @Theory - public void testValidProperties(String value) throws Exception { - Foo foo = createFoo(); - assertThat(foo.bar).isEqualTo("blah"); - assertThat(foo.name).isEqualTo("blah"); - } - - private Foo createFoo() throws Exception { - setupFactory(); - this.factory.setPropertySources(environment.getPropertySources()); - this.factory.afterPropertiesSet(); - return this.factory.getObject(); - } - - private void setupFactory() throws IOException { - this.factory = new PropertiesConfigurationFactory<>(Foo.class); - this.factory.setValidator(this.validator); - this.factory.setTargetName(this.targetName); - this.factory.setIgnoreUnknownFields(this.ignoreUnknownFields); - this.factory.setMessageSource(new StaticMessageSource()); - } - - // Foo needs to be public and to have setters for all properties - public static class Foo { - - @NotNull - private String name; - - private String bar; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public String getBar() { - return this.bar; - } - - public void setBar(String bar) { - this.bar = bar; - } - - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryTests.java deleted file mode 100644 index 06a0136a35..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/PropertiesConfigurationFactoryTests.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * 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.bind; - -import java.io.IOException; -import java.util.Collections; -import java.util.Properties; - -import javax.validation.Validation; -import javax.validation.constraints.NotNull; - -import org.junit.Test; - -import org.springframework.beans.NotWritablePropertyException; -import org.springframework.boot.env.RandomValuePropertySource; -import org.springframework.context.support.StaticMessageSource; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertiesPropertySource; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.core.env.SystemEnvironmentPropertySource; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.support.PropertiesLoaderUtils; -import org.springframework.mock.env.MockPropertySource; -import org.springframework.validation.BindException; -import org.springframework.validation.Validator; -import org.springframework.validation.beanvalidation.SpringValidatorAdapter; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PropertiesConfigurationFactory}. - * - * @author Dave Syer - */ -public class PropertiesConfigurationFactoryTests { - - private PropertiesConfigurationFactory factory; - - private Validator validator; - - private boolean ignoreUnknownFields = true; - - private String targetName = null; - - @Test - public void testValidPropertiesLoadsWithDash() throws Exception { - this.ignoreUnknownFields = false; - Foo foo = createFoo("na-me: blah\nbar: blah"); - assertThat(foo.bar).isEqualTo("blah"); - assertThat(foo.name).isEqualTo("blah"); - } - - @Test - public void testUnknownPropertyOkByDefault() throws Exception { - Foo foo = createFoo("hi: hello\nname: foo\nbar: blah"); - assertThat(foo.bar).isEqualTo("blah"); - } - - @Test(expected = NotWritablePropertyException.class) - public void testUnknownPropertyCausesLoadFailure() throws Exception { - this.ignoreUnknownFields = false; - createFoo("hi: hello\nname: foo\nbar: blah"); - } - - @Test(expected = BindException.class) - public void testMissingPropertyCausesValidationError() throws Exception { - this.validator = new SpringValidatorAdapter( - Validation.buildDefaultValidatorFactory().getValidator()); - createFoo("bar: blah"); - } - - @Test - public void systemEnvironmentBindingFailuresAreIgnored() throws Exception { - setupFactory(); - MutablePropertySources propertySources = new MutablePropertySources(); - MockPropertySource propertySource = new MockPropertySource( - StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); - propertySource.setProperty("doesNotExist", "foo"); - propertySource.setProperty("name", "bar"); - propertySources.addFirst(propertySource); - this.factory.setPropertySources(propertySources); - this.factory.setIgnoreUnknownFields(false); - this.factory.afterPropertiesSet(); - Foo foo = this.factory.getObject(); - assertThat(foo.name).isEqualTo("bar"); - } - - @Test - public void systemEnvironmentBindingWithDefaults() throws Exception { - setupFactory(); - MutablePropertySources propertySources = new MutablePropertySources(); - MockPropertySource propertySource = new MockPropertySource( - StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); - propertySource.setProperty("name", "${foo.name:bar}"); - propertySources.addFirst(propertySource); - this.factory.setPropertySources(propertySources); - this.factory.afterPropertiesSet(); - Foo foo = this.factory.getObject(); - assertThat(foo.name).isEqualTo("bar"); - } - - @Test - public void systemEnvironmentNoResolvePlaceholders() throws Exception { - setupFactory(); - MutablePropertySources propertySources = new MutablePropertySources(); - MockPropertySource propertySource = new MockPropertySource( - StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); - propertySource.setProperty("name", "${foo.name:bar}"); - propertySources.addFirst(propertySource); - this.factory.setPropertySources(propertySources); - this.factory.setResolvePlaceholders(false); - this.factory.afterPropertiesSet(); - Foo foo = this.factory.getObject(); - assertThat(foo.name).isEqualTo("${foo.name:bar}"); - } - - @Test - public void systemPropertyBindingFailuresAreIgnored() throws Exception { - setupFactory(); - MutablePropertySources propertySources = new MutablePropertySources(); - MockPropertySource propertySource = new MockPropertySource( - StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); - propertySource.setProperty("doesNotExist", "foo"); - propertySource.setProperty("name", "bar"); - propertySources.addFirst(propertySource); - this.factory.setPropertySources(propertySources); - this.factory.setIgnoreUnknownFields(false); - this.factory.afterPropertiesSet(); - } - - @Test - public void testBindWithDashPrefix() throws Exception { - // gh-4045 - this.targetName = "foo-bar"; - MutablePropertySources propertySources = new MutablePropertySources(); - propertySources.addLast(new SystemEnvironmentPropertySource("systemEnvironment", - Collections.singletonMap("FOO_BAR_NAME", "blah"))); - propertySources.addLast(new RandomValuePropertySource()); - setupFactory(); - this.factory.setPropertySources(propertySources); - this.factory.afterPropertiesSet(); - Foo foo = this.factory.getObject(); - assertThat(foo.name).isEqualTo("blah"); - } - - @Test - public void testBindWithDelimitedPrefixUsingMatchingDelimiter() throws Exception { - this.targetName = "env_foo"; - this.ignoreUnknownFields = false; - MutablePropertySources propertySources = new MutablePropertySources(); - propertySources.addLast(new SystemEnvironmentPropertySource("systemEnvironment", - Collections.singletonMap("ENV_FOO_NAME", "blah"))); - propertySources.addLast(new RandomValuePropertySource("random")); - setupFactory(); - this.factory.setPropertySources(propertySources); - this.factory.afterPropertiesSet(); - Foo foo = this.factory.getObject(); - assertThat(foo.name).isEqualTo("blah"); - } - - @Test - public void testBindWithDelimitedPrefixUsingDifferentDelimiter() throws Exception { - this.targetName = "env.foo"; - MutablePropertySources propertySources = new MutablePropertySources(); - propertySources.addLast(new SystemEnvironmentPropertySource("systemEnvironment", - Collections.singletonMap("ENV_FOO_NAME", "blah"))); - propertySources.addLast(new RandomValuePropertySource("random")); - this.ignoreUnknownFields = false; - setupFactory(); - this.factory.setPropertySources(propertySources); - this.factory.afterPropertiesSet(); - Foo foo = this.factory.getObject(); - assertThat(foo.name).isEqualTo("blah"); - } - - @Test - public void propertyWithAllUpperCaseSuffixCanBeBound() throws Exception { - Foo foo = createFoo("foo-bar-u-r-i:baz"); - assertThat(foo.fooBarURI).isEqualTo("baz"); - } - - @Test - public void propertyWithAllUpperCaseInTheMiddleCanBeBound() throws Exception { - Foo foo = createFoo("foo-d-l-q-bar:baz"); - assertThat(foo.fooDLQBar).isEqualTo(("baz")); - } - - private Foo createFoo(final String values) throws Exception { - setupFactory(); - return bindFoo(values); - } - - private Foo bindFoo(final String values) throws Exception { - Properties properties = PropertiesLoaderUtils - .loadProperties(new ByteArrayResource(values.getBytes())); - MutablePropertySources propertySources = new MutablePropertySources(); - propertySources.addFirst(new PropertiesPropertySource("test", properties)); - this.factory.setPropertySources(propertySources); - this.factory.afterPropertiesSet(); - return this.factory.getObject(); - } - - private void setupFactory() throws IOException { - this.factory = new PropertiesConfigurationFactory<>(Foo.class); - this.factory.setValidator(this.validator); - this.factory.setTargetName(this.targetName); - this.factory.setIgnoreUnknownFields(this.ignoreUnknownFields); - this.factory.setMessageSource(new StaticMessageSource()); - } - - // Foo needs to be public and to have setters for all properties - public static class Foo { - - @NotNull - private String name; - - private String bar; - - private String spring_foo_baz; - - private String fooBar; - - private String fooBarURI; - - private String fooDLQBar; - - public String getSpringFooBaz() { - return this.spring_foo_baz; - } - - public void setSpringFooBaz(String spring_foo_baz) { - this.spring_foo_baz = spring_foo_baz; - } - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public String getBar() { - return this.bar; - } - - public void setBar(String bar) { - this.bar = bar; - } - - public String getFooBar() { - return this.fooBar; - } - - public void setFooBar(String fooBar) { - this.fooBar = fooBar; - } - - public String getFooBarURI() { - return this.fooBarURI; - } - - public void setFooBarURI(String fooBarURI) { - this.fooBarURI = fooBarURI; - } - - public String getFooDLQBar() { - return this.fooDLQBar; - } - - public void setFooDLQBar(String fooDLQBar) { - this.fooDLQBar = fooDLQBar; - } - - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesBinderTests.java deleted file mode 100644 index 59e60983d4..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesBinderTests.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2012-2016 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.bind; - -import java.util.Map; - -import org.junit.Test; - -import org.springframework.core.env.StandardEnvironment; -import org.springframework.test.context.support.TestPropertySourceUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PropertySourcesBinder}. - * - * @author Stephane Nicoll - */ -public class PropertySourcesBinderTests { - - private StandardEnvironment env = new StandardEnvironment(); - - @Test - public void extractAllWithPrefix() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.env, "foo.first=1", - "foo.second=2"); - Map content = new PropertySourcesBinder(this.env) - .extractAll("foo"); - assertThat(content.get("first")).isEqualTo("1"); - assertThat(content.get("second")).isEqualTo("2"); - assertThat(content).hasSize(2); - } - - @Test - @SuppressWarnings("unchecked") - public void extractNoPrefix() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.env, - "foo.ctx.first=1", "foo.ctx.second=2"); - Map content = new PropertySourcesBinder(this.env).extractAll(""); - assertThat(content.get("foo")).isInstanceOf(Map.class); - Map foo = (Map) content.get("foo"); - assertThat(content.get("foo")).isInstanceOf(Map.class); - Map ctx = (Map) foo.get("ctx"); - assertThat(ctx.get("first")).isEqualTo("1"); - assertThat(ctx.get("second")).isEqualTo("2"); - assertThat(ctx).hasSize(2); - assertThat(foo).hasSize(1); - } - - @Test - public void bindToSimplePojo() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.env, - "test.name=foo", "test.counter=42"); - TestBean bean = new TestBean(); - new PropertySourcesBinder(this.env).bindTo("test", bean); - assertThat(bean.getName()).isEqualTo("foo"); - assertThat(bean.getCounter()).isEqualTo(42); - } - - @SuppressWarnings("unused") - private static class TestBean { - - private String name; - - private Integer counter; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getCounter() { - return this.counter; - } - - public void setCounter(Integer counter) { - this.counter = counter; - } - - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesBindingTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesBindingTests.java deleted file mode 100644 index beb0058945..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesBindingTests.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2012-2016 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.bind; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.bind.PropertySourcesBindingTests.TestConfig; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.PropertySource; -import org.springframework.context.annotation.PropertySources; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PropertySourcesPropertyValues} binding. - * - * @author Dave Syer - */ -@RunWith(SpringRunner.class) -@DirtiesContext -@ContextConfiguration(classes = TestConfig.class, loader = SpringApplicationBindContextLoader.class) -public class PropertySourcesBindingTests { - - @Value("${foo:}") - private String foo; - - @Autowired - private Wrapper properties; - - @Test - public void overridingOfPropertiesOrderOfAtPropertySources() { - assertThat(this.properties.getBar()).isEqualTo("override"); - - } - - @Test - public void overridingOfPropertiesOrderOfAtPropertySourcesWherePropertyIsCapitalized() { - assertThat(this.properties.getSpam()).isEqualTo("BUCKET"); - } - - @Test - public void overridingOfPropertiesOrderOfAtPropertySourcesWherePropertyNamesDiffer() { - assertThat(this.properties.getTheName()).isEqualTo("NAME"); - } - - @Test - public void overridingOfPropertiesAndBindToAtValue() { - assertThat(this.foo).isEqualTo(this.properties.getFoo()); - } - - @Test - public void overridingOfPropertiesOrderOfApplicationProperties() { - assertThat(this.properties.getFoo()).isEqualTo("bucket"); - } - - @Import({ SomeConfig.class }) - @PropertySources({ @PropertySource("classpath:/some.properties"), - @PropertySource("classpath:/override.properties") }) - @Configuration - @EnableConfigurationProperties(Wrapper.class) - public static class TestConfig { - - @Bean - public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { - return new PropertySourcesPlaceholderConfigurer(); - } - - } - - @Configuration - @PropertySources({ @PropertySource("classpath:/override.properties"), - @PropertySource("classpath:/some.properties") }) - public static class SomeConfig { - - } - - @ConfigurationProperties - public static class Wrapper { - - private String foo; - - private String bar; - - private String spam; - - private String theName; - - public String getBar() { - return this.bar; - } - - public void setBar(String bar) { - this.bar = bar; - } - - public String getFoo() { - return this.foo; - } - - public void setFoo(String foo) { - this.foo = foo; - } - - public String getSpam() { - return this.spam; - } - - public void setSpam(String spam) { - this.spam = spam; - } - - public String getTheName() { - return this.theName; - } - - public void setTheName(String theName) { - this.theName = theName; - } - - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesPropertyValuesTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesPropertyValuesTests.java deleted file mode 100644 index 536111c3d5..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/PropertySourcesPropertyValuesTests.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * 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.bind; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.junit.Before; -import org.junit.Test; - -import org.springframework.beans.PropertyValue; -import org.springframework.core.env.CompositePropertySource; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertySource; -import org.springframework.validation.DataBinder; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PropertySourcesPropertyValues}. - * - * @author Dave Syer - * @author Phillip Webb - * @author Stephane Nicoll - */ -public class PropertySourcesPropertyValuesTests { - - private final MutablePropertySources propertySources = new MutablePropertySources(); - - @Before - public void init() { - this.propertySources.addFirst(new PropertySource("static", "foo") { - - @Override - public Object getProperty(String name) { - if (name.equals(getSource())) { - return "bar"; - } - return null; - } - - }); - this.propertySources.addFirst(new MapPropertySource("map", - Collections.singletonMap("name", "${foo}"))); - } - - @Test - public void testTypesPreserved() { - Map map = Collections.singletonMap("name", 123); - this.propertySources.replace("map", new MapPropertySource("map", map)); - PropertySourcesPropertyValues propertyValues = new PropertySourcesPropertyValues( - this.propertySources); - assertThat(propertyValues.getPropertyValues()[0].getValue()).isEqualTo(123); - } - - @Test - public void testSize() { - PropertySourcesPropertyValues propertyValues = new PropertySourcesPropertyValues( - this.propertySources); - assertThat(propertyValues.getPropertyValues().length).isEqualTo(1); - } - - @Test - public void testOrderPreserved() { - LinkedHashMap map = new LinkedHashMap<>(); - map.put("one", 1); - map.put("two", 2); - map.put("three", 3); - map.put("four", 4); - map.put("five", 5); - this.propertySources.addFirst(new MapPropertySource("ordered", map)); - PropertySourcesPropertyValues propertyValues = new PropertySourcesPropertyValues( - this.propertySources); - PropertyValue[] values = propertyValues.getPropertyValues(); - assertThat(values).hasSize(6); - Collection names = new ArrayList<>(); - for (PropertyValue value : values) { - names.add(value.getName()); - } - assertThat(names).containsExactly("one", "two", "three", "four", "five", "name"); - } - - @Test - public void testNonEnumeratedValue() { - PropertySourcesPropertyValues propertyValues = new PropertySourcesPropertyValues( - this.propertySources); - assertThat(propertyValues.getPropertyValue("foo").getValue()).isEqualTo("bar"); - } - - @Test - public void testCompositeValue() { - PropertySource map = this.propertySources.get("map"); - CompositePropertySource composite = new CompositePropertySource("composite"); - composite.addPropertySource(map); - this.propertySources.replace("map", composite); - PropertySourcesPropertyValues propertyValues = new PropertySourcesPropertyValues( - this.propertySources); - assertThat(propertyValues.getPropertyValue("foo").getValue()).isEqualTo("bar"); - } - - @Test - public void testEnumeratedValue() { - PropertySourcesPropertyValues propertyValues = new PropertySourcesPropertyValues( - this.propertySources); - assertThat(propertyValues.getPropertyValue("name").getValue()).isEqualTo("bar"); - } - - @Test - public void testNonEnumeratedPlaceholder() { - this.propertySources.addFirst(new PropertySource("another", "baz") { - - @Override - public Object getProperty(String name) { - if (name.equals(getSource())) { - return "${foo}"; - } - return null; - } - - }); - PropertySourcesPropertyValues propertyValues = new PropertySourcesPropertyValues( - this.propertySources, (Collection) null, - Collections.singleton("baz")); - assertThat(propertyValues.getPropertyValue("baz").getValue()).isEqualTo("bar"); - } - - @Test - public void testOverriddenValue() { - this.propertySources.addFirst(new MapPropertySource("new", - Collections.singletonMap("name", "spam"))); - PropertySourcesPropertyValues propertyValues = new PropertySourcesPropertyValues( - this.propertySources); - assertThat(propertyValues.getPropertyValue("name").getValue()).isEqualTo("spam"); - } - - @Test - public void testPlaceholdersBinding() { - TestBean target = new TestBean(); - DataBinder binder = new DataBinder(target); - binder.bind(new PropertySourcesPropertyValues(this.propertySources)); - assertThat(target.getName()).isEqualTo("bar"); - } - - @Test - public void testPlaceholdersBindingNonEnumerable() { - FooBean target = new FooBean(); - DataBinder binder = new DataBinder(target); - binder.bind(new PropertySourcesPropertyValues(this.propertySources, - (Collection) null, Collections.singleton("foo"))); - assertThat(target.getFoo()).isEqualTo("bar"); - } - - @Test - public void testPlaceholdersBindingWithError() { - TestBean target = new TestBean(); - DataBinder binder = new DataBinder(target); - this.propertySources.addFirst(new MapPropertySource("another", - Collections.singletonMap("something", "${nonexistent}"))); - binder.bind(new PropertySourcesPropertyValues(this.propertySources)); - assertThat(target.getName()).isEqualTo("bar"); - } - - @Test - public void testPlaceholdersErrorInNonEnumerable() { - TestBean target = new TestBean(); - DataBinder binder = new DataBinder(target); - this.propertySources.addFirst(new PropertySource("application", "STUFF") { - - @Override - public Object getProperty(String name) { - return new Object(); - } - - }); - binder.bind(new PropertySourcesPropertyValues(this.propertySources, - (Collection) null, Collections.singleton("name"))); - assertThat(target.getName()).isNull(); - } - - @Test - public void testCollectionProperty() throws Exception { - ListBean target = new ListBean(); - DataBinder binder = new DataBinder(target); - Map map = new LinkedHashMap<>(); - map.put("list[0]", "v0"); - map.put("list[1]", "v1"); - this.propertySources.addFirst(new MapPropertySource("values", map)); - binder.bind(new PropertySourcesPropertyValues(this.propertySources)); - assertThat(target.getList()).containsExactly("v0", "v1"); - } - - @Test - public void testFirstCollectionPropertyWins() throws Exception { - ListBean target = new ListBean(); - DataBinder binder = new DataBinder(target); - Map first = new LinkedHashMap<>(); - first.put("list[0]", "f0"); - Map second = new LinkedHashMap<>(); - second.put("list[0]", "s0"); - second.put("list[1]", "s1"); - this.propertySources.addFirst(new MapPropertySource("s", second)); - this.propertySources.addFirst(new MapPropertySource("f", first)); - binder.bind(new PropertySourcesPropertyValues(this.propertySources)); - assertThat(target.getList()).containsExactly("f0"); - } - - @Test - public void testFirstCollectionPropertyWinsNestedAttributes() throws Exception { - ListTestBean target = new ListTestBean(); - DataBinder binder = new DataBinder(target); - Map first = new LinkedHashMap<>(); - first.put("list[0].description", "another description"); - Map second = new LinkedHashMap<>(); - second.put("list[0].name", "first name"); - second.put("list[0].description", "first description"); - second.put("list[1].name", "second name"); - second.put("list[1].description", "second description"); - this.propertySources.addFirst(new MapPropertySource("s", second)); - this.propertySources.addFirst(new MapPropertySource("f", first)); - binder.bind(new PropertySourcesPropertyValues(this.propertySources)); - assertThat(target.getList()).hasSize(1); - assertThat(target.getList().get(0).getDescription()) - .isEqualTo("another description"); - assertThat(target.getList().get(0).getName()).isNull(); - } - - public static class TestBean { - - private String name; - - private String description; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return this.description; - } - - public void setDescription(String description) { - this.description = description; - } - - } - - public static class FooBean { - - private String foo; - - public String getFoo() { - return this.foo; - } - - public void setFoo(String foo) { - this.foo = foo; - } - - } - - public static class ListBean { - - private List list = new ArrayList<>(); - - public List getList() { - return this.list; - } - - public void setList(List list) { - this.list = list; - } - - } - - public static class ListTestBean { - - private List list = new ArrayList<>(); - - public List getList() { - return this.list; - } - - public void setList(List list) { - this.list = list; - } - - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedNamesTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedNamesTests.java deleted file mode 100644 index ccf0e7a58f..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedNamesTests.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2012-2016 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.bind; - -import java.util.Iterator; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link RelaxedNames}. - * - * @author Phillip Webb - * @author Dave Syer - */ -public class RelaxedNamesTests { - - @Test - public void iterator() throws Exception { - Iterator iterator = new RelaxedNames("my-RELAXED-property").iterator(); - assertThat(iterator.next()).isEqualTo("my-RELAXED-property"); - assertThat(iterator.next()).isEqualTo("my_RELAXED_property"); - assertThat(iterator.next()).isEqualTo("myRELAXEDProperty"); - assertThat(iterator.next()).isEqualTo("myRelaxedProperty"); - assertThat(iterator.next()).isEqualTo("my-relaxed-property"); - assertThat(iterator.next()).isEqualTo("my_relaxed_property"); - assertThat(iterator.next()).isEqualTo("myrelaxedproperty"); - assertThat(iterator.next()).isEqualTo("MY-RELAXED-PROPERTY"); - assertThat(iterator.next()).isEqualTo("MY_RELAXED_PROPERTY"); - assertThat(iterator.next()).isEqualTo("MYRELAXEDPROPERTY"); - assertThat(iterator.hasNext()).isFalse(); - } - - @Test - public void fromUnderscores() throws Exception { - Iterator iterator = new RelaxedNames("nes_ted").iterator(); - assertThat(iterator.next()).isEqualTo("nes_ted"); - assertThat(iterator.next()).isEqualTo("nes.ted"); - assertThat(iterator.next()).isEqualTo("nesTed"); - assertThat(iterator.next()).isEqualTo("nested"); - assertThat(iterator.next()).isEqualTo("NES_TED"); - assertThat(iterator.next()).isEqualTo("NES.TED"); - assertThat(iterator.next()).isEqualTo("NESTED"); - assertThat(iterator.hasNext()).isFalse(); - } - - @Test - public void fromPlain() throws Exception { - Iterator iterator = new RelaxedNames("plain").iterator(); - assertThat(iterator.next()).isEqualTo("plain"); - assertThat(iterator.next()).isEqualTo("PLAIN"); - assertThat(iterator.hasNext()).isFalse(); - } - - @Test - public void fromCamelCase() throws Exception { - Iterator iterator = new RelaxedNames("caMel").iterator(); - assertThat(iterator.next()).isEqualTo("caMel"); - assertThat(iterator.next()).isEqualTo("ca_mel"); - assertThat(iterator.next()).isEqualTo("ca-mel"); - assertThat(iterator.next()).isEqualTo("camel"); - assertThat(iterator.next()).isEqualTo("CAMEL"); - assertThat(iterator.next()).isEqualTo("CA_MEL"); - assertThat(iterator.next()).isEqualTo("CA-MEL"); - assertThat(iterator.hasNext()).isFalse(); - } - - @Test - public void fromCompoundCamelCase() throws Exception { - Iterator iterator = new RelaxedNames("caMelCase").iterator(); - assertThat(iterator.next()).isEqualTo("caMelCase"); - assertThat(iterator.next()).isEqualTo("ca_mel_case"); - assertThat(iterator.next()).isEqualTo("ca-mel-case"); - assertThat(iterator.next()).isEqualTo("camelcase"); - assertThat(iterator.next()).isEqualTo("CAMELCASE"); - assertThat(iterator.next()).isEqualTo("CA_MEL_CASE"); - assertThat(iterator.next()).isEqualTo("CA-MEL-CASE"); - assertThat(iterator.hasNext()).isFalse(); - } - - @Test - public void fromPeriods() throws Exception { - Iterator iterator = new RelaxedNames("spring.value").iterator(); - assertThat(iterator.next()).isEqualTo("spring.value"); - assertThat(iterator.next()).isEqualTo("spring_value"); - assertThat(iterator.next()).isEqualTo("springValue"); - assertThat(iterator.next()).isEqualTo("springvalue"); - assertThat(iterator.next()).isEqualTo("SPRING.VALUE"); - assertThat(iterator.next()).isEqualTo("SPRING_VALUE"); - assertThat(iterator.next()).isEqualTo("SPRINGVALUE"); - assertThat(iterator.hasNext()).isFalse(); - } - - @Test - public void fromPrefixEndingInPeriod() throws Exception { - Iterator iterator = new RelaxedNames("spring.").iterator(); - assertThat(iterator.next()).isEqualTo("spring."); - assertThat(iterator.next()).isEqualTo("spring_"); - assertThat(iterator.next()).isEqualTo("SPRING."); - assertThat(iterator.next()).isEqualTo("SPRING_"); - assertThat(iterator.hasNext()).isFalse(); - } - - @Test - public void fromEmpty() throws Exception { - Iterator iterator = new RelaxedNames("").iterator(); - assertThat(iterator.next()).isEqualTo(""); - assertThat(iterator.hasNext()).isFalse(); - } - - @Test - public void forCamelCase() throws Exception { - Iterator iterator = RelaxedNames.forCamelCase("camelCase").iterator(); - assertThat(iterator.next()).isEqualTo("camel-case"); - assertThat(iterator.next()).isEqualTo("camel_case"); - assertThat(iterator.next()).isEqualTo("camelCase"); - assertThat(iterator.next()).isEqualTo("camelcase"); - assertThat(iterator.next()).isEqualTo("CAMEL-CASE"); - assertThat(iterator.next()).isEqualTo("CAMEL_CASE"); - assertThat(iterator.next()).isEqualTo("CAMELCASE"); - } - - @Test - public void forCamelCaseWithCaps() throws Exception { - Iterator iterator = RelaxedNames.forCamelCase("camelCASE").iterator(); - assertThat(iterator.next()).isEqualTo("camel-c-a-s-e"); - assertThat(iterator.next()).isEqualTo("camel_c_a_s_e"); - assertThat(iterator.next()).isEqualTo("camelCASE"); - assertThat(iterator.next()).isEqualTo("camelcase"); - assertThat(iterator.next()).isEqualTo("CAMEL-C-A-S-E"); - assertThat(iterator.next()).isEqualTo("CAMEL_C_A_S_E"); - assertThat(iterator.next()).isEqualTo("CAMELCASE"); - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedPropertyResolverTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedPropertyResolverTests.java deleted file mode 100644 index a73d0c8829..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedPropertyResolverTests.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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.bind; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Properties; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertiesPropertySource; -import org.springframework.core.env.StandardEnvironment; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link RelaxedPropertyResolver}. - * - * @author Phillip Webb - * @author Stephane Nicoll - */ -public class RelaxedPropertyResolverTests { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private StandardEnvironment environment; - - private RelaxedPropertyResolver resolver; - - private LinkedHashMap source; - - @Before - public void setup() { - this.environment = new StandardEnvironment(); - this.source = new LinkedHashMap<>(); - this.source.put("myString", "value"); - this.source.put("myobject", "object"); - this.source.put("myInteger", 123); - this.source.put("myClass", "java.lang.String"); - this.environment.getPropertySources() - .addFirst(new MapPropertySource("test", this.source)); - this.resolver = new RelaxedPropertyResolver(this.environment); - } - - @Test - public void needsPropertyResolver() throws Exception { - this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("PropertyResolver must not be null"); - new RelaxedPropertyResolver(null); - } - - @Test - public void getRequiredProperty() throws Exception { - assertThat(this.resolver.getRequiredProperty("my-string")).isEqualTo("value"); - this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("required key [my-missing] not found"); - this.resolver.getRequiredProperty("my-missing"); - } - - @Test - public void getRequiredPropertyWithType() throws Exception { - assertThat(this.resolver.getRequiredProperty("my-integer", Integer.class)) - .isEqualTo(123); - this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("required key [my-missing] not found"); - this.resolver.getRequiredProperty("my-missing", Integer.class); - } - - @Test - public void getProperty() throws Exception { - assertThat(this.resolver.getProperty("my-string")).isEqualTo("value"); - assertThat(this.resolver.getProperty("my-missing")).isNull(); - } - - @Test - public void getPropertyNoSeparator() throws Exception { - assertThat(this.resolver.getProperty("myobject")).isEqualTo("object"); - assertThat(this.resolver.getProperty("my-object")).isEqualTo("object"); - } - - @Test - public void getPropertyWithDefault() throws Exception { - assertThat(this.resolver.getProperty("my-string", "a")).isEqualTo("value"); - assertThat(this.resolver.getProperty("my-missing", "a")).isEqualTo("a"); - } - - @Test - public void getPropertyWithType() throws Exception { - assertThat(this.resolver.getProperty("my-integer", Integer.class)).isEqualTo(123); - assertThat(this.resolver.getProperty("my-missing", Integer.class)).isNull(); - } - - @Test - public void getPropertyWithTypeAndDefault() throws Exception { - assertThat(this.resolver.getProperty("my-integer", Integer.class, 345)) - .isEqualTo(123); - assertThat(this.resolver.getProperty("my-missing", Integer.class, 345)) - .isEqualTo(345); - } - - @Test - public void containsProperty() throws Exception { - assertThat(this.resolver.containsProperty("my-string")).isTrue(); - assertThat(this.resolver.containsProperty("myString")).isTrue(); - assertThat(this.resolver.containsProperty("my_string")).isTrue(); - assertThat(this.resolver.containsProperty("my-missing")).isFalse(); - } - - @Test - public void resolverPlaceholder() throws Exception { - this.thrown.expect(UnsupportedOperationException.class); - this.resolver.resolvePlaceholders("test"); - } - - @Test - public void resolveRequiredPlaceholders() throws Exception { - this.thrown.expect(UnsupportedOperationException.class); - this.resolver.resolveRequiredPlaceholders("test"); - } - - @Test - public void prefixed() throws Exception { - this.resolver = new RelaxedPropertyResolver(this.environment, "a.b.c."); - this.source.put("a.b.c.d", "test"); - assertThat(this.resolver.containsProperty("d")).isTrue(); - assertThat(this.resolver.getProperty("d")).isEqualTo("test"); - } - - @Test - public void prefixedRelaxed() throws Exception { - this.resolver = new RelaxedPropertyResolver(this.environment, "a."); - this.source.put("A_B", "test"); - this.source.put("a.foobar", "spam"); - assertThat(this.resolver.containsProperty("b")).isTrue(); - assertThat(this.resolver.getProperty("b")).isEqualTo("test"); - assertThat(this.resolver.getProperty("foo-bar")).isEqualTo("spam"); - } - - @Test - public void subProperties() throws Exception { - this.source.put("x.y.my-sub.a.b", "1"); - this.source.put("x.y.mySub.a.c", "2"); - this.source.put("x.y.MY_SUB.a.d", "3"); - this.resolver = new RelaxedPropertyResolver(this.environment, "x.y."); - Map subProperties = this.resolver.getSubProperties("my-sub."); - assertThat(subProperties.size()).isEqualTo(3); - assertThat(subProperties.get("a.b")).isEqualTo("1"); - assertThat(subProperties.get("a.c")).isEqualTo("2"); - assertThat(subProperties.get("a.d")).isEqualTo("3"); - } - - @Test - public void testPropertySource() throws Exception { - Properties properties; - PropertiesPropertySource propertySource; - String propertyPrefix = "spring.datasource."; - String propertyName = "password"; - String fullPropertyName = propertyPrefix + propertyName; - StandardEnvironment environment = new StandardEnvironment(); - MutablePropertySources sources = environment.getPropertySources(); - properties = new Properties(); - properties.put(fullPropertyName, "systemPassword"); - propertySource = new PropertiesPropertySource("system", properties); - sources.addLast(propertySource); - properties = new Properties(); - properties.put(fullPropertyName, "propertiesPassword"); - propertySource = new PropertiesPropertySource("properties", properties); - sources.addLast(propertySource); - RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver( - environment, propertyPrefix); - String directProperty = propertyResolver.getProperty(propertyName); - Map subProperties = propertyResolver.getSubProperties(""); - String subProperty = (String) subProperties.get(propertyName); - assertThat(subProperty).isEqualTo(directProperty); - } - -} diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/SimplerPropertySourcesBindingTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/SimplerPropertySourcesBindingTests.java deleted file mode 100644 index c5d7185226..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/bind/SimplerPropertySourcesBindingTests.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2012-2016 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.bind; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.bind.SimplerPropertySourcesBindingTests.TestConfig; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.context.annotation.PropertySources; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PropertySourcesPropertyValues} binding. - * - * @author Dave Syer - */ -@RunWith(SpringRunner.class) -@DirtiesContext -@ContextConfiguration(classes = TestConfig.class, loader = SpringApplicationBindContextLoader.class) -public class SimplerPropertySourcesBindingTests { - - @Value("${foo:}") - private String foo; - - @Autowired - private Wrapper properties; - - @Test - public void overridingOfPropertiesWorksAsExpected() { - assertThat(this.foo).isEqualTo(this.properties.getFoo()); - } - - @PropertySources({ @PropertySource("classpath:/override.properties"), - @PropertySource("classpath:/some.properties") }) - @Configuration - @EnableConfigurationProperties(Wrapper.class) - public static class TestConfig { - - @Bean - public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { - return new PropertySourcesPlaceholderConfigurer(); - } - - } - - @ConfigurationProperties - public static class Wrapper { - - private String foo; - - public String getFoo() { - return this.foo; - } - - public void setFoo(String foo) { - this.foo = foo; - } - - } - -}