diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java b/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java index 54e0037e97..a18b188813 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcher.java @@ -21,22 +21,34 @@ import java.util.HashSet; import java.util.Set; /** - * Default {@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 '.' - * or '_'. This implementation is optimized for frequent calls. + * {@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; - public DefaultPropertyNamePatternsMatcher(String... names) { - this(new HashSet(Arrays.asList(names))); + protected DefaultPropertyNamePatternsMatcher(char[] delimiters, String... names) { + this(delimiters, false, names); } - public DefaultPropertyNamePatternsMatcher(Set names) { + protected DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase, + String... names) { + this(delimiters, ignoreCase, new HashSet(Arrays.asList(names))); + } + + public DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase, + Set names) { + this.delimiters = delimiters; + this.ignoreCase = ignoreCase; this.names = names.toArray(new String[names.size()]); } @@ -55,18 +67,19 @@ class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher return false; } for (int charIndex = 0; charIndex < propertyNameChars.length; charIndex++) { - noneMatched = true; for (int nameIndex = 0; nameIndex < this.names.length; nameIndex++) { if (match[nameIndex]) { + match[nameIndex] = false; if (charIndex < this.names[nameIndex].length()) { - if (this.names[nameIndex].charAt(charIndex) == propertyNameChars[charIndex]) { + if (isCharMatch(this.names[nameIndex].charAt(charIndex), + propertyNameChars[charIndex])) { match[nameIndex] = true; noneMatched = false; } } else { char charAfter = propertyNameChars[this.names[nameIndex].length()]; - if (charAfter == '.' || charAfter == '_') { + if (isDelimeter(charAfter)) { match[nameIndex] = true; noneMatched = false; } @@ -85,4 +98,20 @@ class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher return false; } + private boolean isCharMatch(char c1, char c2) { + if (this.ignoreCase) { + return Character.toLowerCase(c1) == Character.toLowerCase(c2); + } + return c1 == c2; + } + + private boolean isDelimeter(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/SimplePropertyNamePatternsMatcher.java b/spring-boot/src/main/java/org/springframework/boot/bind/PatternPropertyNamePatternsMatcher.java similarity index 88% rename from spring-boot/src/main/java/org/springframework/boot/bind/SimplePropertyNamePatternsMatcher.java rename to spring-boot/src/main/java/org/springframework/boot/bind/PatternPropertyNamePatternsMatcher.java index b7fd6380e2..be6976e2a7 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/SimplePropertyNamePatternsMatcher.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PatternPropertyNamePatternsMatcher.java @@ -27,11 +27,11 @@ import org.springframework.util.PatternMatchUtils; * @author Phillip Webb * @since 1.2.0 */ -class SimplePropertyNamePatternsMatcher implements PropertyNamePatternsMatcher { +class PatternPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher { private final String[] patterns; - public SimplePropertyNamePatternsMatcher(Collection patterns) { + public PatternPropertyNamePatternsMatcher(Collection patterns) { this.patterns = (patterns == null ? new String[] {} : patterns .toArray(new String[patterns.size()])); } diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java index 662cd4a17b..b9582f62ec 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java @@ -17,8 +17,9 @@ 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; @@ -34,6 +35,7 @@ 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; @@ -51,6 +53,10 @@ import org.springframework.validation.Validator; public class PropertiesConfigurationFactory implements FactoryBean, MessageSourceAware, InitializingBean { + private static final char[] EXACT_DELIMETERS = { '_', '.', '[' }; + + private static final char[] TARGET_NAME_DELIMETERS = { '_', '.' }; + private final Log logger = LogFactory.getLog(getClass()); private boolean ignoreUnknownFields = true; @@ -257,16 +263,28 @@ public class PropertiesConfigurationFactory implements FactoryBean, } private Set getNames() { - Set names = new HashSet(); + Set names = new LinkedHashSet(); if (this.target != null) { + Iterable prefixes = (StringUtils.hasLength(this.targetName) ? new RelaxedNames( + this.targetName) : null); PropertyDescriptor[] descriptors = BeanUtils .getPropertyDescriptors(this.target.getClass()); - String prefix = (this.targetName != null ? this.targetName + "." : ""); for (PropertyDescriptor descriptor : descriptors) { String name = descriptor.getName(); if (!name.equals("class")) { - for (String relaxedName : new RelaxedNames(prefix + name)) { - names.add(relaxedName); + 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); + } + } } } } @@ -278,8 +296,33 @@ public class PropertiesConfigurationFactory implements FactoryBean, if (this.properties != null) { return new MutablePropertyValues(this.properties); } - return new PropertySourcesPropertyValues(this.propertySources, - new DefaultPropertyNamePatternsMatcher(names), names); + return getPropertySourcesPropertyValues(names); + } + + private PropertyValues getPropertySourcesPropertyValues(Set names) { + PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names); + return new PropertySourcesPropertyValues(this.propertySources, names, includes); + } + + private PropertyNamePatternsMatcher getPropertyNamePatternsMatcher(Set names) { + 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_DELIMETERS, true, names); + } + if (this.targetName != 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 + return new DefaultPropertyNamePatternsMatcher(TARGET_NAME_DELIMETERS, + this.targetName); + } + // 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 validate(RelaxedDataBinder dataBinder) throws BindException { 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 ee5474c4c4..7d186f7024 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 @@ -24,6 +24,15 @@ package org.springframework.boot.bind; */ interface PropertyNamePatternsMatcher { + PropertyNamePatternsMatcher ALL = new PropertyNamePatternsMatcher() { + + @Override + public boolean matches(String propertyName) { + return true; + } + + }; + PropertyNamePatternsMatcher NONE = new PropertyNamePatternsMatcher() { @Override 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 9d9c8721ac..6d88c62fa4 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 @@ -16,7 +16,6 @@ package org.springframework.boot.bind; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -30,7 +29,7 @@ import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.core.env.PropertySourcesPropertyResolver; -import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.Assert; import org.springframework.validation.DataBinder; /** @@ -46,17 +45,16 @@ public class PropertySourcesPropertyValues implements PropertyValues { private final PropertySources propertySources; - private static final Collection PATTERN_MATCHED_PROPERTY_SOURCES = Arrays - .asList(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, - StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); + private final Collection propertyNames; + + private final PropertyNamePatternsMatcher includes; /** * Create a new PropertyValues from the given PropertySources * @param propertySources a PropertySources instance */ public PropertySourcesPropertyValues(PropertySources propertySources) { - this(propertySources, (PropertyNamePatternsMatcher) null, - (Collection) null); + this(propertySources, (Collection) null, PropertyNamePatternsMatcher.ALL); } /** @@ -64,52 +62,55 @@ public class PropertySourcesPropertyValues implements PropertyValues { * @param propertySources a PropertySources instance * @param includePatterns property name patterns to include from system properties and * environment variables - * @param names exact property names to include + * @param propertyNames the property names to used in lieu of an + * {@link EnumerablePropertySource}. */ public PropertySourcesPropertyValues(PropertySources propertySources, - Collection includePatterns, Collection names) { - this(propertySources, new SimplePropertyNamePatternsMatcher(includePatterns), - names); + Collection includePatterns, Collection propertyNames) { + this(propertySources, propertyNames, new PatternPropertyNamePatternsMatcher( + includePatterns)); } /** * Create a new PropertyValues from the given PropertySources * @param propertySources a PropertySources instance - * @param includes property name patterns to include from system properties and - * environment variables - * @param names exact property names to include + * @param propertyNames the property names to used in lieu of an + * {@link EnumerablePropertySource}. + * @param includes the property name patterns to include */ PropertySourcesPropertyValues(PropertySources propertySources, - PropertyNamePatternsMatcher includes, Collection names) { + Collection propertyNames, PropertyNamePatternsMatcher includes) { + Assert.notNull(propertySources, "PropertySources must not be null"); + Assert.notNull(includes, "Includes must not be null"); this.propertySources = propertySources; - if (includes == null) { - includes = PropertyNamePatternsMatcher.NONE; - } - if (names == null) { - names = Collections.emptySet(); - } + this.propertyNames = (propertyNames == null ? Collections. emptySet() + : propertyNames); + this.includes = includes; PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver( propertySources); for (PropertySource source : propertySources) { - processPropertySource(source, resolver, includes, names); + processPropertySource(source, resolver); } } private void processPropertySource(PropertySource source, - PropertySourcesPropertyResolver resolver, - PropertyNamePatternsMatcher includes, Collection exacts) { + PropertySourcesPropertyResolver resolver) { if (source instanceof CompositePropertySource) { - processCompositePropertySource((CompositePropertySource) source, resolver, - includes, exacts); + processCompositePropertySource((CompositePropertySource) source, resolver); } else if (source instanceof EnumerablePropertySource) { processEnumerablePropertySource((EnumerablePropertySource) source, - resolver, includes); + resolver, this.includes); } else { - // We can only do exact matches for non-enumerable property names, but - // that's better than nothing... - processDefaultPropertySource(source, resolver, includes, exacts); + processNonEnumerablePropertySource(source, resolver); + } + } + + private void processCompositePropertySource(CompositePropertySource source, + PropertySourcesPropertyResolver resolver) { + for (PropertySource nested : source.getPropertySources()) { + processPropertySource(nested, resolver); } } @@ -117,14 +118,9 @@ public class PropertySourcesPropertyValues implements PropertyValues { PropertySourcesPropertyResolver resolver, PropertyNamePatternsMatcher includes) { if (source.getPropertyNames().length > 0) { for (String propertyName : source.getPropertyNames()) { - if (PropertySourcesPropertyValues.PATTERN_MATCHED_PROPERTY_SOURCES - .contains(source.getName()) && !includes.matches(propertyName)) { - continue; - } - Object value = getEnumerableProperty(source, resolver, propertyName); - if (!this.propertyValues.containsKey(propertyName)) { - this.propertyValues.put(propertyName, new PropertyValue(propertyName, - value)); + if (includes.matches(propertyName)) { + Object value = getEnumerableProperty(source, resolver, propertyName); + putIfAbsent(propertyName, value); } } } @@ -141,18 +137,11 @@ public class PropertySourcesPropertyValues implements PropertyValues { } } - private void processCompositePropertySource(CompositePropertySource source, - PropertySourcesPropertyResolver resolver, - PropertyNamePatternsMatcher includes, Collection exacts) { - for (PropertySource nested : source.getPropertySources()) { - processPropertySource(nested, resolver, includes, exacts); - } - } - - private void processDefaultPropertySource(PropertySource source, - PropertySourcesPropertyResolver resolver, - PropertyNamePatternsMatcher includes, Collection exacts) { - for (String propertyName : exacts) { + private void processNonEnumerablePropertySource(PropertySource source, + PropertySourcesPropertyResolver resolver) { + // We can only do exact matches for non-enumerable property names, but + // that's better than nothing... + for (String propertyName : this.propertyNames) { if (!source.containsProperty(propertyName)) { continue; } @@ -166,11 +155,13 @@ public class PropertySourcesPropertyValues implements PropertyValues { if (value == null) { value = source.getProperty(propertyName.toUpperCase()); } - if (value != null && !this.propertyValues.containsKey(propertyName)) { - this.propertyValues.put(propertyName, new PropertyValue(propertyName, - value)); - continue; - } + putIfAbsent(propertyName, value); + } + } + + private void putIfAbsent(String propertyName, Object value) { + if (value != null && !this.propertyValues.containsKey(propertyName)) { + this.propertyValues.put(propertyName, new PropertyValue(propertyName, value)); } } 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 f239eeb14c..ccc6ace4a0 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 @@ -188,4 +188,13 @@ public final class RelaxedNames implements Iterable { } } + /** + * Return a {@link RelaxedNames} for the given source camelCase source name + * @param name the source name in camelCase + * @return the relaxed names + */ + public static RelaxedNames forCamelCase(String name) { + return new RelaxedNames(Manipulation.CAMELCASE_TO_HYPHEN.apply(name)); + } + } 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 index 869c3a7906..6247e483f6 100644 --- a/spring-boot/src/test/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcherTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/bind/DefaultPropertyNamePatternsMatcherTests.java @@ -28,42 +28,53 @@ import static org.junit.Assert.assertTrue; */ public class DefaultPropertyNamePatternsMatcherTests { + private static final char[] DELIMITERS = { '.', '_' }; + @Test public void namesShorter() { - assertFalse(new DefaultPropertyNamePatternsMatcher("aaaa", "bbbb") + assertFalse(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb") .matches("zzzzz")); } @Test public void namesExactMatch() { - assertTrue(new DefaultPropertyNamePatternsMatcher("aaaa", "bbbb", "cccc") - .matches("bbbb")); + assertTrue(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb", + "cccc").matches("bbbb")); } @Test public void namesLonger() { - assertFalse(new DefaultPropertyNamePatternsMatcher("aaaaa", "bbbbb", "ccccc") - .matches("bbbb")); + assertFalse(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaaa", "bbbbb", + "ccccc").matches("bbbb")); } @Test public void nameWithDot() throws Exception { - assertTrue(new DefaultPropertyNamePatternsMatcher("aaaa", "bbbb", "cccc") - .matches("bbbb.anything")); + assertTrue(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb", + "cccc").matches("bbbb.anything")); } @Test public void nameWithUnderscore() throws Exception { - assertTrue(new DefaultPropertyNamePatternsMatcher("aaaa", "bbbb", "cccc") - .matches("bbbb_anything")); + assertTrue(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb", + "cccc").matches("bbbb_anything")); } @Test public void namesMatchWithDifferentLengths() throws Exception { - assertTrue(new DefaultPropertyNamePatternsMatcher("aaa", "bbbb", "ccccc") - .matches("bbbb")); + assertTrue(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaa", "bbbb", + "ccccc").matches("bbbb")); + } + @Test + public void withSquareBrackets() throws Exception { + char[] delimeters = "._[".toCharArray(); + PropertyNamePatternsMatcher matcher = new DefaultPropertyNamePatternsMatcher( + delimeters, "aaa", "bbbb", "ccccc"); + assertTrue(matcher.matches("bbbb")); + assertTrue(matcher.matches("bbbb[4]")); + assertFalse(matcher.matches("bbb[4]")); } } diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java index 310d2eef4b..230233fbb7 100644 --- a/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java @@ -102,6 +102,20 @@ public class RelaxedDataBinderTests { assertEquals("bar", target.getFoo()); } + @Test + public void testBindToCamelCaseFromEnvironmentStyleWithPrefix() throws Exception { + VanillaTarget target = new VanillaTarget(); + bind(target, "TEST_FOO_BAZ: bar", "test"); + assertEquals("bar", target.getFooBaz()); + } + + @Test + public void testBindToCamelCaseFromEnvironmentStyle() throws Exception { + VanillaTarget target = new VanillaTarget(); + bind(target, "test.FOO_BAZ: bar", "test"); + assertEquals("bar", target.getFooBaz()); + } + @Test public void testBindFromEnvironmentStyleWithNestedPrefix() throws Exception { VanillaTarget target = new VanillaTarget();