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 15068140ec..250be706d4 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 @@ -37,6 +37,7 @@ import org.springframework.validation.DataBinder; * case for example). * * @author Dave Syer + * @see RelaxedNames */ public class RelaxedDataBinder extends DataBinder { @@ -224,76 +225,19 @@ public class RelaxedDataBinder extends DataBinder { private String getActualPropertyName(BeanWrapper target, String prefix, String name) { prefix = StringUtils.hasText(prefix) ? prefix + "." : ""; - for (Variation variation : Variation.values()) { - for (Manipulation manipulation : Manipulation.values()) { - // Apply all manipulations before attempting variations - String candidate = variation.apply(manipulation.apply(name)); - try { - if (target.getPropertyType(prefix + candidate) != null) { - return candidate; - } - } - catch (InvalidPropertyException ex) { - // swallow and continue + for (String candidate : new RelaxedNames(name)) { + try { + if (target.getPropertyType(prefix + candidate) != null) { + return candidate; } } + catch (InvalidPropertyException ex) { + // swallow and continue + } } return name; } - static enum Variation { - NONE { - @Override - public String apply(String value) { - return value; - } - }, - UPPERCASE { - @Override - public String apply(String value) { - return value.toUpperCase(); - } - }, - - LOWERCASE { - @Override - public String apply(String value) { - return value.toLowerCase(); - } - }; - - public abstract String apply(String value); - } - - static enum Manipulation { - NONE { - @Override - public String apply(String value) { - return value; - } - }, - UNDERSCORE { - @Override - public String apply(String value) { - return value.replace("-", "_"); - } - }, - - CAMELCASE { - @Override - public String apply(String value) { - StringBuilder builder = new StringBuilder(); - for (String field : UNDERSCORE.apply(value).split("_")) { - builder.append(builder.length() == 0 ? field : StringUtils - .capitalize(field)); - } - return builder.toString(); - } - }; - - public abstract String apply(String value); - } - static class MapHolder { private Map map; 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 new file mode 100644 index 0000000000..40ec2df61f --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedNames.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2013 the original author 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 java.util.NoSuchElementException; + +import org.springframework.util.StringUtils; + +/** + * Generates relaxed name variations from a given source. + * + * @author Phillip Webb + * @author Dave Syer + * @see RelaxedDataBinder + * @see RelaxedPropertyResolver + */ +public final class RelaxedNames implements Iterable { + + private final String name; + + /** + * Create a new {@link RelaxedNames} instance. + * + * @param name the source name. For the maximum number of variations specify the name + * using dashed notation (e.g. {@literal my-property-name} + */ + public RelaxedNames(String name) { + this.name = name; + } + + @Override + public Iterator iterator() { + return new RelaxedNamesIterator(); + } + + private class RelaxedNamesIterator implements Iterator { + + private int variation = 0; + + private int manipulation = 0; + + @Override + public boolean hasNext() { + return (this.variation < Variation.values().length); + } + + @Override + public String next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + String result = RelaxedNames.this.name; + result = Manipulation.values()[this.manipulation].apply(result); + result = Variation.values()[this.variation].apply(result); + this.manipulation++; + if (this.manipulation >= Manipulation.values().length) { + this.variation++; + this.manipulation = 0; + } + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + } + + static enum Variation { + NONE { + @Override + public String apply(String value) { + return value; + } + }, + LOWERCASE { + @Override + public String apply(String value) { + return value.toLowerCase(); + } + }, + UPPERCASE { + @Override + public String apply(String value) { + return value.toUpperCase(); + } + }; + + public abstract String apply(String value); + } + + static enum Manipulation { + NONE { + @Override + public String apply(String value) { + return value; + } + }, + UNDERSCORE { + @Override + public String apply(String value) { + return value.replace("-", "_"); + } + }, + CAMELCASE { + @Override + public String apply(String value) { + StringBuilder builder = new StringBuilder(); + for (String field : UNDERSCORE.apply(value).split("_")) { + builder.append(builder.length() == 0 ? field : StringUtils + .capitalize(field)); + } + return builder.toString(); + } + }; + + public abstract String apply(String value); + } +} 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 new file mode 100644 index 0000000000..541adf7938 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java @@ -0,0 +1,194 @@ +/* + * Copyright 2012-2013 the original author 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.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySources; +import org.springframework.util.Assert; + +import static java.lang.String.format; + +/** + * {@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, 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) { + for (String relaxedKey : new RelaxedNames(key)) { + if (this.resolver.containsProperty(this.prefix + relaxedKey)) { + return this.resolver.getProperty(this.prefix + relaxedKey, targetType, + defaultValue); + } + } + return defaultValue; + } + + @Override + public Class getPropertyAsClass(String key, Class targetType) { + for (String relaxedKey : new RelaxedNames(key)) { + if (this.resolver.containsProperty(this.prefix + relaxedKey)) { + return this.resolver.getPropertyAsClass(this.prefix + relaxedKey, + targetType); + } + } + return null; + } + + @Override + public boolean containsProperty(String key) { + for (String relaxedKey : new RelaxedNames(key)) { + if (this.resolver.containsProperty(this.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 in 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 #getSubProperties(PropertySources, RelaxedNames) + * @see #getSubProperties(PropertySources, String, RelaxedNames) + */ + public Map getSubProperties(String keyPrefix) { + Assert.isInstanceOf(ConfigurableEnvironment.class, this.resolver, + "SubProperties not available."); + ConfigurableEnvironment env = (ConfigurableEnvironment) this.resolver; + return getSubProperties(env.getPropertySources(), this.prefix, new RelaxedNames( + 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 keyPrefix the key prefixes to test + * @return a map of all sub properties starting with the specified key prefixes. + * @see #getSubProperties(PropertySources, String, RelaxedNames) + */ + public static Map getSubProperties(PropertySources propertySources, + RelaxedNames keyPrefix) { + return 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 keyPrefex (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, RelaxedNames) + */ + public static Map getSubProperties(PropertySources propertySources, + String rootPrefix, RelaxedNames keyPrefix) { + Map subProperties = new LinkedHashMap(); + for (PropertySource source : propertySources) { + if (source instanceof EnumerablePropertySource) { + for (String name : ((EnumerablePropertySource) source) + .getPropertyNames()) { + String key = getSubKey(name, rootPrefix, keyPrefix); + if (key != null) { + subProperties.put(key, source.getProperty(name)); + } + } + } + } + return Collections.unmodifiableMap(subProperties); + } + + private static String getSubKey(String name, String rootPrefix, RelaxedNames keyPrefix) { + rootPrefix = (rootPrefix == null ? "" : rootPrefix); + for (String candidateKeyPrefix : keyPrefix) { + if (name.startsWith(rootPrefix + candidateKeyPrefix)) { + return name.substring((rootPrefix + candidateKeyPrefix).length()); + } + } + return null; + } + +} 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 new file mode 100644 index 0000000000..458ae77bfc --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedNamesTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2013 the original author 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.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link RelaxedNames}. + * + * @author Phillip Webb + */ +public class RelaxedNamesTests { + + @Test + public void iterator() throws Exception { + Iterator iterator = new RelaxedNames("my-RELAXED-property").iterator(); + assertThat(iterator.next(), equalTo("my-RELAXED-property")); + assertThat(iterator.next(), equalTo("my_RELAXED_property")); + assertThat(iterator.next(), equalTo("myRELAXEDProperty")); + assertThat(iterator.next(), equalTo("my-relaxed-property")); + assertThat(iterator.next(), equalTo("my_relaxed_property")); + assertThat(iterator.next(), equalTo("myrelaxedproperty")); + assertThat(iterator.next(), equalTo("MY-RELAXED-PROPERTY")); + assertThat(iterator.next(), equalTo("MY_RELAXED_PROPERTY")); + assertThat(iterator.next(), equalTo("MYRELAXEDPROPERTY")); + assertThat(iterator.hasNext(), equalTo(false)); + + iterator = new RelaxedNames("nes_ted").iterator(); + assertThat(iterator.next(), equalTo("nes_ted")); + assertThat(iterator.next(), equalTo("nes_ted")); + assertThat(iterator.next(), equalTo("nesTed")); + assertThat(iterator.next(), equalTo("nes_ted")); + assertThat(iterator.next(), equalTo("nes_ted")); + assertThat(iterator.next(), equalTo("nested")); + assertThat(iterator.next(), equalTo("NES_TED")); + assertThat(iterator.next(), equalTo("NES_TED")); + assertThat(iterator.next(), equalTo("NESTED")); + assertThat(iterator.hasNext(), equalTo(false)); + } + +} 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 new file mode 100644 index 0000000000..fdcbd3c22b --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedPropertyResolverTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2013 the original author 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.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.StandardEnvironment; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link RelaxedPropertyResolver}. + * + * @author Phillip Webb + */ +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("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"), equalTo("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), + equalTo(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"), equalTo("value")); + assertThat(this.resolver.getProperty("my-missing"), nullValue()); + } + + @Test + public void getPropertyWithDefault() throws Exception { + assertThat(this.resolver.getProperty("my-string", "a"), equalTo("value")); + assertThat(this.resolver.getProperty("my-missing", "a"), equalTo("a")); + } + + @Test + public void getPropertyWithType() throws Exception { + assertThat(this.resolver.getProperty("my-integer", Integer.class), equalTo(123)); + assertThat(this.resolver.getProperty("my-missing", Integer.class), nullValue()); + } + + @Test + public void getPropertyWithTypeAndDefault() throws Exception { + assertThat(this.resolver.getProperty("my-integer", Integer.class, 345), + equalTo(123)); + assertThat(this.resolver.getProperty("my-missing", Integer.class, 345), + equalTo(345)); + } + + @Test + public void getPropertyAsClass() throws Exception { + assertThat(this.resolver.getPropertyAsClass("my-class", String.class), + equalTo(String.class)); + assertThat(this.resolver.getPropertyAsClass("my-missing", String.class), + nullValue()); + } + + @Test + public void containsProperty() throws Exception { + assertThat(this.resolver.containsProperty("my-string"), equalTo(true)); + assertThat(this.resolver.containsProperty("myString"), equalTo(true)); + assertThat(this.resolver.containsProperty("my_string"), equalTo(true)); + assertThat(this.resolver.containsProperty("my-missing"), equalTo(false)); + } + + @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"), equalTo(true)); + assertThat(this.resolver.getProperty("d"), equalTo("test")); + } + + @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(), equalTo(3)); + assertThat(subProperties.get("a.b"), equalTo((Object) "1")); + assertThat(subProperties.get("a.c"), equalTo((Object) "2")); + assertThat(subProperties.get("a.d"), equalTo((Object) "3")); + } +}