From ae330c19d57b41ef51a4f993d73951a64f2b1b46 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 22 Aug 2013 01:41:10 -0700 Subject: [PATCH] Create RelaxedPropertyResolver Create RelaxedPropertyResolver class that can be used to get values from another PropertyResolver (probably an Environment) using the same relaxed rules as the RelaxedDataBinder. The commit extracts the relaxed naming rules from RelaxedDataBinder into a new RelaxedNames class. Issue: #55621278 --- .../boot/bind/RelaxedDataBinder.java | 72 +------ .../boot/bind/RelaxedNames.java | 135 ++++++++++++ .../boot/bind/RelaxedPropertyResolver.java | 194 ++++++++++++++++++ .../boot/bind/RelaxedNamesTests.java | 60 ++++++ .../bind/RelaxedPropertyResolverTests.java | 159 ++++++++++++++ 5 files changed, 556 insertions(+), 64 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/bind/RelaxedNames.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/bind/RelaxedPropertyResolver.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/RelaxedNamesTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/bind/RelaxedPropertyResolverTests.java 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 15068140ec4..250be706d4c 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 00000000000..40ec2df61f1 --- /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 00000000000..541adf7938d --- /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 00000000000..458ae77bfcf --- /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 00000000000..fdcbd3c22b8 --- /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")); + } +}