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); 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); 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/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/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/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 { 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"); } 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")); } 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(); } 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; } 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 8579230fe1..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,9 @@ 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; import org.springframework.context.ApplicationListener; @@ -54,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; @@ -75,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; @@ -375,6 +373,7 @@ public class SpringApplication { && this.webApplicationType == WebApplicationType.NONE) { environment = convertToStandardEnvironment(environment); } + ConfigurationPropertySources.attach(environment); return environment; } @@ -576,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()); } @@ -589,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/bind/DefaultPropertyNamePatternsMatcher.java b/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java deleted file mode 100644 index 58b80d2f93..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java +++ /dev/null @@ -1,118 +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 - */ -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/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 deleted file mode 100644 index be8fd600d8..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java +++ /dev/null @@ -1,345 +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 - */ -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/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 deleted file mode 100644 index fb30e91d90..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourceUtils.java +++ /dev/null @@ -1,91 +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 - */ -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 8e59e80ea9..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesBinder.java +++ /dev/null @@ -1,128 +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 - */ -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/PropertySourcesPropertyValues.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java index ce0b73cab5..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; @@ -41,6 +43,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 @@ -107,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; 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..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,8 +30,8 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Dave Syer * @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 deleted file mode 100644 index 327165a620..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java +++ /dev/null @@ -1,155 +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.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 - */ -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/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/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/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 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/env/OriginCapablePropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java similarity index 52% rename from spring-boot/src/main/java/org/springframework/boot/env/OriginCapablePropertySource.java rename to spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java index 34d3032426..5bc2900d58 100644 --- a/spring-boot/src/main/java/org/springframework/boot/env/OriginCapablePropertySource.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/PlaceholdersResolver.java @@ -14,26 +14,31 @@ * limitations under the License. */ -package org.springframework.boot.env; +package org.springframework.boot.context.properties.bind; -import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertyResolver; /** - * 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. + * Optional strategy that used by a {@link Binder} to resolve property placeholders. * * @author Phillip Webb + * @author Madhura Bhave * @since 2.0.0 + * @see PropertySourcesPlaceholdersResolver */ -public interface OriginCapablePropertySource { +@FunctionalInterface +public interface PlaceholdersResolver { /** - * 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} + * No-op {@link PropertyResolver}. */ - PropertyOrigin getPropertyOrigin(String name); + 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/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/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/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/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/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/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/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 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; - } - - } - -} 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..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 @@ -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; @@ -34,16 +36,20 @@ 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; 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; -import org.springframework.validation.BindException; import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; @@ -59,6 +65,7 @@ import static org.junit.Assert.fail; * @author Christian Dupuis * @author Phillip Webb * @author Stephane Nicoll + * @author Madhura Bhave */ public class ConfigurationPropertiesBindingPostProcessorTests { @@ -83,7 +90,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 +113,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 +203,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 +220,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 +275,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 +292,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 +308,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 +327,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(); @@ -355,14 +368,63 @@ 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(); + 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(); 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 +469,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests { } @ConfigurationProperties(prefix = "test") + @Validated public static class PropertyWithoutJSR303 implements Validator { private String foo; @@ -469,6 +532,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 +544,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 +638,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests { } @ConfigurationProperties(prefix = "custom") + @Validated public static class PropertyWithCustomValidator { private String foo; @@ -647,6 +737,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests { @Configuration @EnableConfigurationProperties @ConfigurationProperties(prefix = "test") + @Validated public static class PropertyWithValue { @Value("${default.value}") @@ -669,6 +760,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests { @Configuration @EnableConfigurationProperties + @Validated @ConfigurationProperties(prefix = "test") public static class PropertiesWithMap { @@ -689,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 { 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/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..971d227446 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -0,0 +1,376 @@ +/* + * Copyright 2012-2017 the original author 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.ArgumentCaptor; +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 static final Bindable> STRING_ARRAY_MAP = Bindable + .mapOf(String.class, String[].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)); + } + + @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)); + } + +} 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/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(); + } + +} 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; + } + + } + } 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/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/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()); 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); } 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