From 961d41f6f69839ea861e58aef1ad7b203da83801 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 8 May 2017 11:31:20 -0700 Subject: [PATCH] Reduce ConfigurationPropertyName memory usage Significantly rework `ConfigurationPropertyName` in an attempt to reduce the amount of memory and garbage produced. The name elements are now stored as CharSequences and whenever possible subsequences are used. This helps to reduce the memory footprint since the underlying char array can be shared between the source string, and the individual elements. For example: `ConfigurationProperty.of("foo.bar.baz")` will return a name that provides access to the elements `foo`, `bar` and `baz`. However, these three names all share the same char[], just using different offsets and lengths. See gh-9000 --- .../properties/bind/AggregateBinder.java | 16 - .../bind/IndexedElementsBinder.java | 13 +- .../context/properties/bind/MapBinder.java | 20 +- .../bind/validation/ValidationErrors.java | 7 +- .../source/ConfigurationPropertyName.java | 779 +++++++++++------- .../ConfigurationPropertyNameBuilder.java | 181 ---- .../source/DefaultPropertyMapper.java | 15 +- ...FilteredConfigurationPropertiesSource.java | 3 +- ...IterableConfigurationPropertiesSource.java | 3 +- .../IterableConfigurationPropertySource.java | 3 +- .../properties/source/PropertyMapper.java | 8 +- .../SpringConfigurationPropertySource.java | 6 +- ...ngIterableConfigurationPropertySource.java | 3 +- .../SystemEnvironmentPropertyMapper.java | 51 +- .../FileEncodingApplicationListenerTests.java | 1 + .../properties/bind/MapBinderTests.java | 1 - ...ConfigurationPropertyNameBuilderTests.java | 116 --- .../ConfigurationPropertyNameTests.java | 413 +++++++--- 18 files changed, 852 insertions(+), 787 deletions(-) delete mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilder.java delete mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilderTests.java 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 index 64a6842d71..29c7540084 100644 --- 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 @@ -84,22 +84,6 @@ abstract class AggregateBinder { 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 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 index 296b00bf76..702a9a36d2 100644 --- 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 @@ -42,6 +42,8 @@ import org.springframework.util.MultiValueMap; */ abstract class IndexedElementsBinder extends AggregateBinder { + private static final String INDEX_ZERO = "[0]"; + IndexedElementsBinder(BindContext context) { super(context); } @@ -81,12 +83,13 @@ abstract class IndexedElementsBinder extends AggregateBinder { MultiValueMap knownIndexedChildren = getKnownIndexedChildren( source, root); for (int i = 0; i < Integer.MAX_VALUE; i++) { - ConfigurationPropertyName name = root.appendIndex(i); + ConfigurationPropertyName name = root + .append(i == 0 ? INDEX_ZERO : "[" + i + "]"); Object value = elementBinder.bind(name, Bindable.of(elementType), source); if (value == null) { break; } - knownIndexedChildren.remove(name.getElement().getValue(Form.UNIFORM)); + knownIndexedChildren.remove(name.getLastElement(Form.UNIFORM)); collection.get().add(value); } assertNoUnboundChildren(knownIndexedChildren); @@ -100,9 +103,9 @@ abstract class IndexedElementsBinder extends AggregateBinder { } for (ConfigurationPropertyName name : (IterableConfigurationPropertySource) source .filter(root::isAncestorOf)) { - name = rollUp(name, root); - if (name.getElement().isIndexed()) { - String key = name.getElement().getValue(Form.UNIFORM); + name = name.chop(root.getNumberOfElements() + 1); + if (name.isLastElementIndexed()) { + String key = name.getLastElement(Form.UNIFORM); ConfigurationProperty value = source.getConfigurationProperty(name); children.add(key, value); } 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 index a80a775829..124468745a 100644 --- 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 @@ -18,7 +18,6 @@ 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; @@ -97,7 +96,7 @@ class MapBinder extends AggregateBinder> { } private Bindable getValueBindable(ConfigurationPropertyName name) { - if (isMultiElementName(name) && isValueTreatedAsNestedMap()) { + if (!this.root.isParentOf(name) && isValueTreatedAsNestedMap()) { return Bindable.of(this.mapType); } return Bindable.of(this.valueType); @@ -105,17 +104,13 @@ class MapBinder extends AggregateBinder> { private ConfigurationPropertyName getEntryName(ConfigurationPropertySource source, ConfigurationPropertyName name) { - if (isMultiElementName(name) + if (!this.root.isParentOf(name) && (isValueTreatedAsNestedMap() || !isScalarValue(source, name))) { - return rollUp(name, this.root); + return name.chop(this.root.getNumberOfElements() + 1); } 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)); } @@ -139,8 +134,13 @@ class MapBinder extends AggregateBinder> { } private String getKeyName(ConfigurationPropertyName name) { - return name.stream(this.root).map((e) -> e.getValue(Form.ORIGINAL)) - .collect(Collectors.joining(".")); + StringBuilder result = new StringBuilder(); + for (int i = this.root.getNumberOfElements(); i < name + .getNumberOfElements(); i++) { + result.append(result.length() == 0 ? "" : "."); + result.append(name.getElement(i, Form.ORIGINAL)); + } + return result.toString(); } } 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 index 7001fb05c8..ba0e921327 100644 --- 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 @@ -22,8 +22,6 @@ 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; @@ -97,8 +95,9 @@ public class ValidationErrors implements Iterable { private boolean isForError(ConfigurationPropertyName name, ConfigurationPropertyName boundPropertyName, FieldError error) { - return Objects.areEqual(boundPropertyName.getParent(), name) && boundPropertyName - .getElement().getValue(Form.UNIFORM).equalsIgnoreCase(error.getField()); + return name.isParentOf(boundPropertyName) + && boundPropertyName.getLastElement(Form.UNIFORM).toString() + .equalsIgnoreCase(error.getField()); } /** 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 index 60ffefb544..2b07bf8ce5 100644 --- 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 @@ -16,23 +16,20 @@ package org.springframework.boot.context.properties.source; +import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; +import java.util.List; import java.util.Map; -import java.util.regex.Pattern; -import java.util.stream.Stream; +import java.util.function.Function; -import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Element; -import org.springframework.boot.context.properties.source.ConfigurationPropertyNameBuilder.ElementValueProcessor; 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}") and "{@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. + * A configuration property name composed of elements separated by dots. User created + * names may contain the characters "{@code a-z}" "{@code 0-9}") and "{@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 @@ -44,127 +41,206 @@ import org.springframework.util.StringUtils; *

  • {@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 #of(CharSequence) * @see ConfigurationPropertySource */ public final class ConfigurationPropertyName - implements Iterable, Comparable { + implements Comparable { + + private static final String EMPTY_STRING = ""; /** * An empty {@link ConfigurationPropertyName}. */ public static final ConfigurationPropertyName EMPTY = new ConfigurationPropertyName( - null, new Element()); + new String[0]); - private static final ConfigurationPropertyNameBuilder BUILDER = new ConfigurationPropertyNameBuilder( - ElementValueProcessor.identity().withValidName()); + private final CharSequence[] elements; - private final ConfigurationPropertyName parent; + private final CharSequence[] uniformElements; - private final Element element; + private int[] elementHashCodes; - private String toString; + private String string; - ConfigurationPropertyName(ConfigurationPropertyName parent, Element element) { - Assert.notNull(element, "Element must not be null"); - this.parent = (parent == EMPTY ? null : parent); - this.element = element; + private ConfigurationPropertyName(CharSequence[] elements) { + this(elements, new CharSequence[elements.length]); + } + + private ConfigurationPropertyName(CharSequence[] elements, + CharSequence[] uniformElements) { + this.elements = elements; + this.uniformElements = uniformElements; } /** - * Return the parent of this configuration property. - * @return the parent or {code null} + * Returns {@code true} if this {@link ConfigurationPropertyName} is empty. + * @return {@code true} if the name is empty */ - public ConfigurationPropertyName getParent() { - return this.parent; + public boolean isEmpty() { + return this.elements.length == 0; } /** - * Return the element part of this configuration property name. - * @return the element (never {@code null}) + * Return if the last element in the name is indexed. + * @return {@code true} if the last element is indexed */ - public Element getElement() { - return this.element; - } - - @Override - public Iterator iterator() { - return stream().iterator(); + public boolean isLastElementIndexed() { + int size = getNumberOfElements(); + return (size > 0 && isIndexed(this.elements[size - 1])); } /** - * Return a stream of the {@link Element Elements} that make up this name. - * @return a stream of {@link Element} items + * Return if the an element in the name is indexed. + * @param elementIndex the index of the element + * @return {@code true} if the last element is indexed */ - public Stream stream() { - if (this.parent == null) { - return Stream.of(this.element); + boolean isIndexed(int elementIndex) { + return isIndexed(this.elements[elementIndex]); + } + + /** + * Return the last element in the name in the given form. + * @param form the form to return + * @return the last element + */ + public String getLastElement(Form form) { + int size = getNumberOfElements(); + return (size == 0 ? EMPTY_STRING : getElement(size - 1, form)); + } + + /** + * Return an element in the name in the given form. + * @param elementIndex the element index + * @param form the form to return + * @return the last element + */ + public String getElement(int elementIndex, Form form) { + if (form == Form.ORIGINAL) { + CharSequence result = this.elements[elementIndex]; + if (isIndexed(result)) { + result = result.subSequence(1, result.length() - 1); + } + return result.toString(); } - 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); + CharSequence result = this.uniformElements[elementIndex]; + if (result == null) { + result = this.elements[elementIndex]; + if (isIndexed(result)) { + result = result.subSequence(1, result.length() - 1); + } + else { + result = cleanupCharSequence(result, (c, i) -> c == '-' || c == '_', + CharProcessor.LOWERCASE); + } + this.uniformElements[elementIndex] = result; } - 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 + * Return the total number of elements in the name. + * @return the number of elements + */ + public int getNumberOfElements() { + return this.elements.length; + } + + /** + * Create a new {@link ConfigurationPropertyName} by appending the given element + * value. + * @param elementValue the single element value to append + * @return a new {@link ConfigurationPropertyName} + */ + public ConfigurationPropertyName append(String elementValue) { + if (elementValue == null) { + return this; + } + process(elementValue, '.', (value, start, end, indexed) -> Assert.isTrue( + start == 0, + () -> "Element value '" + elementValue + "' must be a single item")); + Assert.isTrue( + isIndexed(elementValue) || ElementValidator.isValidElement(elementValue), + () -> "Element value '" + elementValue + "' is not valid"); + int length = this.elements.length; + CharSequence[] elements = new CharSequence[length + 1]; + System.arraycopy(this.elements, 0, elements, 0, length); + elements[length] = elementValue; + CharSequence[] uniformElements = new CharSequence[length + 1]; + System.arraycopy(this.uniformElements, 0, uniformElements, 0, length); + return new ConfigurationPropertyName(elements, uniformElements); + } + + /** + * Return a new {@link ConfigurationPropertyName} by chopping this name to the given + * {@code size}. For example, {@code chop(1)} on the name {@code foo.bar} will return + * {@code foo}. + * @param size the size to chop + * @return the chopped name + */ + public ConfigurationPropertyName chop(int size) { + if (size >= getNumberOfElements()) { + return this; + } + CharSequence[] elements = new CharSequence[size]; + System.arraycopy(this.elements, 0, elements, 0, size); + CharSequence[] uniformElements = new CharSequence[size]; + System.arraycopy(this.uniformElements, 0, uniformElements, 0, size); + return new ConfigurationPropertyName(elements, uniformElements); + } + + /** + * Returns {@code true} if this element is an immediate parent of the specified name. + * @param name the name to check + * @return {@code true} if this name is an ancestor + */ + public boolean isParentOf(ConfigurationPropertyName name) { + Assert.notNull(name, "Name must not be null"); + if (this.getNumberOfElements() != name.getNumberOfElements() - 1) { + return false; + } + return isAncestorOf(name); + } + + /** + * Returns {@code true} if this element is an ancestor (immediate or nested parent) of * the specified name. * @param name the name to check * @return {@code true} if this name is an ancestor */ public boolean isAncestorOf(ConfigurationPropertyName name) { - if (this.equals(EMPTY)) { - return true; + Assert.notNull(name, "Name must not be null"); + if (this.getNumberOfElements() >= name.getNumberOfElements()) { + return false; } - ConfigurationPropertyName candidate = (name == null ? null : name.getParent()); - while (candidate != null) { - if (candidate.equals(this)) { - return true; + for (int i = 0; i < this.elements.length; i++) { + if (!elementEquals(this.elements[i], name.elements[i])) { + return false; } - candidate = candidate.getParent(); } - return false; + return true; } @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); + return compare(this, other); + } + + private int compare(ConfigurationPropertyName n1, ConfigurationPropertyName n2) { + int l1 = n1.getNumberOfElements(); + int l2 = n2.getNumberOfElements(); + int i1 = 0; + int i2 = 0; + while (i1 < l1 || i2 < l2) { + boolean indexed1 = (i1 < l1 ? n1.isIndexed(i2) : false); + boolean indexed2 = (i2 < l2 ? n2.isIndexed(i2) : false); + String e1 = (i1 < l1 ? n1.getElement(i1++, Form.UNIFORM) : null); + String e2 = (i2 < l2 ? n2.getElement(i2++, Form.UNIFORM) : null); + int result = compare(e1, indexed1, e2, indexed2); if (result != 0) { return result; } @@ -172,97 +248,169 @@ public final class ConfigurationPropertyName return 0; } - private int compare(Element element, Element other) { - if (element == null) { + private int compare(String e1, boolean indexed1, String e2, boolean indexed2) { + if (e1 == null) { return -1; } - if (other == null) { + if (e2 == null) { return 1; } - return element.compareTo(other); + int result = Boolean.compare(indexed2, indexed1); + if (result != 0) { + return result; + } + if (indexed1 && indexed2) { + try { + long v1 = Long.parseLong(e1.toString()); + long v2 = Long.parseLong(e2.toString()); + return Long.compare(v1, v2); + } + catch (NumberFormatException ex) { + // Fallback to string comparison + } + } + return e1.compareTo(e2); + } + + @Override + public String toString() { + if (this.string == null) { + this.string = toString(this.elements); + } + return this.string; + } + + private String toString(CharSequence[] elements) { + StringBuilder result = new StringBuilder(); + for (CharSequence element : elements) { + boolean indexed = isIndexed(element); + if (result.length() > 0 && !indexed) { + result.append("."); + } + if (indexed) { + result.append(element); + } + else { + for (int i = 0; i < element.length(); i++) { + char ch = Character.toLowerCase(element.charAt(i)); + result.append(ch == '_' ? "" : ch); + } + } + } + return result.toString(); } @Override public int hashCode() { - int result = 1; - result = 31 * result + ObjectUtils.nullSafeHashCode(this.parent); - result = 31 * result + ObjectUtils.nullSafeHashCode(this.element); - return result; + if (this.elementHashCodes == null) { + this.elementHashCodes = getElementHashCodes(); + } + return ObjectUtils.nullSafeHashCode(this.elementHashCodes); + } + + private int[] getElementHashCodes() { + int[] hashCodes = new int[this.elements.length]; + for (int i = 0; i < this.elements.length; i++) { + hashCodes[i] = getElementHashCode(this.elements[i]); + } + return hashCodes; + } + + private int getElementHashCode(CharSequence element) { + int hash = 0; + boolean indexed = isIndexed(element); + int offset = (indexed ? 1 : 0); + for (int i = 0 + offset; i < element.length() - offset; i++) { + char ch = (indexed ? element.charAt(i) + : Character.toLowerCase(element.charAt(i))); + hash = (ch == '-' || ch == '_' ? hash : 31 * hash + Character.hashCode(ch)); + } + return hash; } @Override public boolean equals(Object obj) { - if (this == obj) { + if (obj == this) { return true; } - if (obj == null || getClass() != obj.getClass()) { + if (obj == null || !obj.getClass().equals(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 BUILDER.from(this, 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, element); + if (getNumberOfElements() != other.getNumberOfElements()) { + return false; } - return this; + for (int i = 0; i < this.elements.length; i++) { + if (!elementEquals(this.elements[i], other.elements[i])) { + return false; + } + } + return true; + } + + private boolean elementEquals(CharSequence e1, CharSequence e2) { + int l1 = e1.length(); + int l2 = e2.length(); + boolean indexed1 = isIndexed(e1); + int offset1 = (indexed1 ? 1 : 0); + boolean indexed2 = isIndexed(e2); + int offset2 = (indexed2 ? 1 : 0); + int i1 = offset1; + int i2 = offset2; + while (i1 < l1 - offset1) { + if (i2 >= l2 - offset2) { + return false; + } + char ch1 = (indexed1 ? e1.charAt(i1) : Character.toLowerCase(e1.charAt(i1))); + char ch2 = (indexed2 ? e2.charAt(i2) : Character.toLowerCase(e2.charAt(i2))); + if (ch1 == '-' || ch1 == '_') { + i1++; + } + else if (ch2 == '-' || ch2 == '_') { + i2++; + } + else if (ch1 != ch2) { + return false; + } + else { + i1++; + i2++; + } + } + while (i2 < l2 - offset2) { + char ch = e2.charAt(i2++); + if (ch != '-' && ch != '_') { + return false; + } + } + return true; + } + + private static boolean isIndexed(CharSequence element) { + int length = element.length(); + return length > 2 && element.charAt(0) == '[' + && element.charAt(length - 1) == ']'; } /** * Returns if the given name is valid. If this method returns {@code true} then the - * name may be used with {@link #of(String)} without throwing an exception. + * name may be used with {@link #of(CharSequence)} without throwing an exception. * @param name the name to test * @return {@code true} if the name is valid */ - public static boolean isValid(String name) { + public static boolean isValid(CharSequence name) { if (name == null) { return false; } - boolean indexed = false; - int charIndex = 0; - for (int i = 0; i < name.length(); i++) { - char ch = name.charAt(i); - if (!indexed) { - if (ch == '[') { - indexed = true; - charIndex = 1; - } - else if (ch == '.') { - charIndex = 0; - } - else { - if (!Element.isValid(charIndex, ch)) { - return false; - } - charIndex++; - } - } - else { - if (ch == ']') { - indexed = false; - charIndex = 0; - } - } + if (name.equals(EMPTY_STRING)) { + return true; } - return true; + if (name.charAt(0) == '.' || name.charAt(name.length() - 1) == '.') { + return false; + } + ElementValidator validator = new ElementValidator(); + process(name, '.', validator); + return validator.isValid(); } /** @@ -271,142 +419,137 @@ public final class ConfigurationPropertyName * @return a {@link ConfigurationPropertyName} instance * @throws IllegalArgumentException if the name is not valid */ - public static ConfigurationPropertyName of(String name) - throws IllegalArgumentException { + public static ConfigurationPropertyName of(CharSequence name) { Assert.notNull(name, "Name must not be null"); - Assert.isTrue(!name.startsWith("."), "Name must not start with '.'"); - Assert.isTrue(!name.endsWith("."), "Name must not end with '.'"); - if (StringUtils.isEmpty(name)) { + if (name.length() >= 1 + && (name.charAt(0) == '.' || name.charAt(name.length() - 1) == '.')) { + throw new IllegalArgumentException( + "Configuration property name '" + name + "' is not valid"); + } + if (name.length() == 0) { return EMPTY; } - return BUILDER.from(name, '.'); + List elements = new ArrayList(10); + process(name, '.', (elementValue, start, end, indexed) -> { + if (elementValue.length() > 0) { + Assert.isTrue(indexed || ElementValidator.isValidElement(elementValue), + () -> "Configuration property name '" + name + "' is not valid"); + elements.add(elementValue); + } + }); + return new ConfigurationPropertyName( + elements.toArray(new CharSequence[elements.size()])); } /** - * An individual element of the {@link ConfigurationPropertyName}. + * Create a {@link ConfigurationPropertyName} by adapting the given source. See + * {@link #adapt(CharSequence, char, Function)} for details. + * @param name the name to parse + * @param separator the separator used to split the name + * @return a {@link ConfigurationPropertyName} */ - public static final class Element implements Comparable { + static ConfigurationPropertyName adapt(CharSequence name, char separator) { + return adapt(name, separator, Function.identity()); + } - 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); + /** + * Create a {@link ConfigurationPropertyName} by adapting the given source. The name + * is split into elements around the given {@code separator}. This method is more + * lenient than {@link #of} in that it allows mixed case names and '{@code _}' + * characters. Other invalid characters are stripped out during parsing. + *

    + * The {@code elementValueProcessor} function may be used if additional processing is + * required on the extracted element values. + * @param name the name to parse + * @param separator the separator used to split the name + * @param elementValueProcessor a function to process element values + * @return a {@link ConfigurationPropertyName} + */ + static ConfigurationPropertyName adapt(CharSequence name, char separator, + Function elementValueProcessor) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(elementValueProcessor, "ElementValueProcessor must not be null"); + if (name.length() == 0) { + return EMPTY; } - - Element(String value) { - Assert.notNull(value, "Value must not be null"); - this.indexed = isIndexed(value); - value = (this.indexed ? value.substring(1, value.length() - 1) : value); - if (!this.indexed) { - validate(value); + List elements = new ArrayList(10); + process(name, separator, (elementValue, start, end, indexed) -> { + elementValue = elementValueProcessor.apply(elementValue); + if (!isIndexed(elementValue)) { + elementValue = cleanupCharSequence(elementValue, + (ch, index) -> ch != '_' && !ElementValidator + .isValidChar(Character.toLowerCase(ch), index), + CharProcessor.NONE); } - 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 (elementValue.length() > 0) { + elements.add(elementValue); } - 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 new ConfigurationPropertyName( + elements.toArray(new CharSequence[elements.size()])); + } + + private static void process(CharSequence name, char separator, + ElementProcessor processor) { + int start = 0; + boolean indexed = false; + int length = name.length(); + for (int i = 0; i < length; i++) { + char ch = name.charAt(i); + if (indexed && ch == ']') { + processElement(processor, name, start, i + 1, indexed); + start = i + 1; + indexed = false; } - 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; + else if (!indexed && ch == '[') { + processElement(processor, name, start, i, indexed); + start = i; + indexed = true; } - if (obj == null || getClass() != obj.getClass()) { - return false; + else if (!indexed && ch == separator) { + processElement(processor, name, start, i, indexed); + start = i + 1; } - return ObjectUtils.nullSafeEquals(getValue(Form.UNIFORM), - ((Element) obj).getValue(Form.UNIFORM)); } + processElement(processor, name, start, length, false); + } - @Override - public String toString() { - String string = getValue(Form.CONFIGURATION); - return (this.indexed ? "[" + string + "]" : string); + private static void processElement(ElementProcessor processor, CharSequence name, + int start, int end, boolean indexed) { + if ((end - start) >= 1) { + processor.process(name.subSequence(start, end), start, end, indexed); } + } - /** - * 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()]; - } - - static boolean isValid(String value) { - if (!isIndexed(value)) { - for (int i = 0; i < value.length(); i++) { - if (!isValid(i, value.charAt(i))) { - return false; + private static CharSequence cleanupCharSequence(CharSequence name, CharFilter filter, + CharProcessor processor) { + for (int i = 0; i < name.length(); i++) { + char ch = name.charAt(i); + char processed = processor.process(ch, i); + if (filter.isExcluded(processed, i) || processed != ch) { + // We save memory by only creating the new result if necessary + StringBuilder result = new StringBuilder(name.length()); + result.append(name.subSequence(0, i)); + for (int j = i; j < name.length(); j++) { + processed = processor.process(name.charAt(j), j); + if (!filter.isExcluded(processed, j)) { + result.append(processed); } } + return result; } - return true; } - - static boolean isValid(int index, char ch) { - boolean isAlpha = ch >= 'a' && ch <= 'z'; - boolean isNumeric = ch >= '0' && ch <= '9'; - if (index == 0) { - return isAlpha; - } - return isAlpha || isNumeric || ch == '-'; - } - - private static boolean isIndexed(String value) { - return value.startsWith("[") && value.endsWith("]"); - } - + return name; } /** - * The various forms that a non-indexed {@link Element} {@code value} can take. + * The various forms that a non-indexed element value can take. */ public enum Form { /** - * The original form as specified when the name was created. For example: + * The original form as specified when the name was created or parsed. For + * example: *

      *
    • "{@code foo-bar}" = "{@code foo-bar}"
    • *
    • "{@code fooBar}" = "{@code fooBar}"
    • @@ -414,33 +557,7 @@ public final class ConfigurationPropertyName *
    • "{@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 == '-'); - } - - }, + ORIGINAL, /** * The uniform configuration form (used for equals/hashCode; lower-case with only @@ -452,53 +569,77 @@ public final class ConfigurationPropertyName *
  • "{@code [Foo.bar]}" = "{@code Foo.bar}"
  • * */ - UNIFORM { + UNIFORM - @Override - protected boolean isIncluded(char ch) { - return Character.isAlphabetic(ch) || Character.isDigit(ch); + } + + /** + * Internal functional interface used when processing names. + */ + @FunctionalInterface + private interface ElementProcessor { + + void process(CharSequence elementValue, int start, int end, boolean indexed); + + } + + /** + * Internal filter used to strip out characters. + */ + private interface CharFilter { + + boolean isExcluded(char ch, int index); + + } + + /** + * Internal processor used to change characters. + */ + private interface CharProcessor { + + CharProcessor NONE = (c, i) -> c; + + CharProcessor LOWERCASE = (c, i) -> Character.toLowerCase(c); + + char process(char c, int index); + + } + + /** + * {@link ElementProcessor} that checks if a name is valid. + */ + private static class ElementValidator implements ElementProcessor { + + private boolean valid = true; + + @Override + public void process(CharSequence elementValue, int start, int end, + boolean indexed) { + if (this.valid && !indexed) { + this.valid = isValidElement(elementValue); } - - }; - - /** - * 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) { + public boolean isValid() { + return this.valid; + } + + public static boolean isValidElement(CharSequence elementValue) { + for (int i = 0; i < elementValue.length(); i++) { + if (!isValidChar(elementValue.charAt(i), i)) { + return false; + } + } 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)); + public static boolean isValidChar(char ch, int index) { + boolean isAlpha = ch >= 'a' && ch <= 'z'; + boolean isNumeric = ch >= '0' && ch <= '9'; + if (index == 0) { + return isAlpha; } - return result; + return isAlpha || isNumeric || ch == '-'; } } 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 deleted file mode 100644 index 888ac2fa54..0000000000 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilder.java +++ /dev/null @@ -1,181 +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.context.properties.source; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -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.ConcurrentReferenceHashMap; - -/** - * 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 - * @see ConfigurationPropertyName - */ -class ConfigurationPropertyNameBuilder { - - private static final Element INDEX_ZERO_ELEMENT = new Element("[0]"); - - private final ElementValueProcessor processor; - - private final Map nameCache = new ConcurrentReferenceHashMap<>(); - - ConfigurationPropertyNameBuilder() { - this(ElementValueProcessor.identity()); - } - - ConfigurationPropertyNameBuilder(ElementValueProcessor processor) { - Assert.notNull(processor, "Processor must not be null"); - this.processor = processor; - } - - /** - * Build 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 ConfigurationPropertyName from(String name, char separator) { - Assert.notNull(name, "Name must not be null"); - ConfigurationPropertyName result = this.nameCache.get(name); - if (result != null) { - return result; - } - 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); - result = from(elements.stream().filter(Objects::nonNull) - .filter((e) -> !e.getValue(Form.UNIFORM).isEmpty()).iterator()); - this.nameCache.put(name, result); - return result; - } - - private void addElement(List elements, StringBuilder value) { - if (value.length() > 0) { - elements.add(buildElement(value.toString())); - value.setLength(0); - } - } - - private ConfigurationPropertyName from(Iterator elements) { - ConfigurationPropertyName name = null; - while (elements.hasNext()) { - name = new ConfigurationPropertyName(name, elements.next()); - } - Assert.state(name != null, "At least one element must be defined"); - return name; - } - - public ConfigurationPropertyName from(ConfigurationPropertyName parent, int index) { - if (index == 0) { - return new ConfigurationPropertyName(parent, INDEX_ZERO_ELEMENT); - } - return from(parent, "[" + index + "]"); - - } - - public ConfigurationPropertyName from(ConfigurationPropertyName parent, - String elementValue) { - return new ConfigurationPropertyName(parent, buildElement(elementValue)); - } - - private Element buildElement(String value) { - return new Element(this.processor.apply(value)); - } - - /** - * An processor that will be applied to element values. Can be used to manipulate or - * restrict the values that are used. - */ - 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; - - /** - * Return an empty {@link ElementValueProcessor} that simply returns the original - * value unchanged. - * @return an empty {@link ElementValueProcessor}. - */ - static ElementValueProcessor identity() { - return (value) -> value; - } - - /** - * Extend this processor with a to enforce standard element name rules. - * @return an element processor that additionally enforces a valid name - */ - default ElementValueProcessor withValidName() { - return (value) -> { - value = apply(value); - if (!Element.isValid(value)) { - throw new IllegalArgumentException( - "Element value '" + value + "' is not valid"); - } - return value; - }; - } - - } - -} 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 index 5259c5e4aa..30c3ad04ae 100644 --- 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 @@ -40,8 +40,6 @@ final class DefaultPropertyMapper implements PropertyMapper { private LastMapping lastMappedPropertyName; - private final ConfigurationPropertyNameBuilder nameBuilder = new ConfigurationPropertyNameBuilder(); - private DefaultPropertyMapper() { } @@ -76,14 +74,17 @@ final class DefaultPropertyMapper implements PropertyMapper { private List tryMap(String propertySourceName) { try { - ConfigurationPropertyName convertedName = this.nameBuilder - .from(propertySourceName, '.'); - PropertyMapping o = new PropertyMapping(propertySourceName, convertedName); - return Collections.singletonList(o); + ConfigurationPropertyName convertedName = ConfigurationPropertyName + .adapt(propertySourceName, '.'); + if (!convertedName.isEmpty()) { + PropertyMapping o = new PropertyMapping(propertySourceName, + convertedName); + return Collections.singletonList(o); + } } catch (Exception ex) { - return Collections.emptyList(); } + return Collections.emptyList(); } private static class LastMapping { 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 index eb274be338..91584108cb 100644 --- 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 @@ -48,7 +48,8 @@ class FilteredConfigurationPropertiesSource implements ConfigurationPropertySour } @Override - public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { + public ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { ConfigurationPropertyState result = this.source.containsDescendantOf(name); if (result == ConfigurationPropertyState.PRESENT) { // We can't be sure a contained descendant won't be filtered diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredIterableConfigurationPropertiesSource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredIterableConfigurationPropertiesSource.java index d2e645930f..f936b4ab14 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredIterableConfigurationPropertiesSource.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredIterableConfigurationPropertiesSource.java @@ -47,7 +47,8 @@ class FilteredIterableConfigurationPropertiesSource } @Override - public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { + public ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { return ConfigurationPropertyState.search(this, name::isAncestorOf); } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/IterableConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/IterableConfigurationPropertySource.java index abae5e3ab3..7b9446917e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/IterableConfigurationPropertySource.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/IterableConfigurationPropertySource.java @@ -61,7 +61,8 @@ public interface IterableConfigurationPropertySource Stream stream(); @Override - default ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { + default ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { return ConfigurationPropertyState.search(this, name::isAncestorOf); } 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 index 30ff036c6e..b0646ea196 100644 --- 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 @@ -27,10 +27,10 @@ import org.springframework.core.env.PropertySource; *

    * Mappings should be provided for both {@link ConfigurationPropertyName * ConfigurationPropertyName} types and {@code String} based names. This allows the - * {@link SpringConfigurationPropertySource} 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 SpringConfigurationPropertySource} 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 SpringConfigurationPropertySource} for more details. * diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySource.java index 3f1ccb6ffc..ab9eeffee1 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySource.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySource.java @@ -89,7 +89,8 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource { } @Override - public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { + public ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { return this.containsDescendantOfMethod.apply(name); } @@ -176,7 +177,8 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource { PropertySource source) { if (source instanceof RandomValuePropertySource) { return (name) -> (name.isAncestorOf(RANDOM) || name.equals(RANDOM) - ? ConfigurationPropertyState.PRESENT : ConfigurationPropertyState.ABSENT); + ? ConfigurationPropertyState.PRESENT + : ConfigurationPropertyState.ABSENT); } return null; } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java index abebcd303d..23c1282410 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java @@ -87,7 +87,8 @@ class SpringIterableConfigurationPropertySource extends SpringConfigurationPrope } @Override - public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { + public ConfigurationPropertyState containsDescendantOf( + ConfigurationPropertyName name) { return ConfigurationPropertyState.search(this, name::isAncestorOf); } 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 index c8f4dcd561..c070559f42 100644 --- 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 @@ -20,7 +20,6 @@ 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; @@ -47,9 +46,6 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper { public static final PropertyMapper INSTANCE = new SystemEnvironmentPropertyMapper(); - private final ConfigurationPropertyNameBuilder nameBuilder = new ConfigurationPropertyNameBuilder( - this::createElement); - private SystemEnvironmentPropertyMapper() { } @@ -57,7 +53,7 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper { public List map(PropertySource propertySource, String propertySourceName) { ConfigurationPropertyName name = convertName(propertySourceName); - if (name == null) { + if (name == null || name.isEmpty()) { return Collections.emptyList(); } if (propertySourceName.endsWith("__")) { @@ -69,7 +65,8 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper { private ConfigurationPropertyName convertName(String propertySourceName) { try { - return this.nameBuilder.from(propertySourceName, '_'); + return ConfigurationPropertyName.adapt(propertySourceName, '_', + this::processElementValue); } catch (Exception ex) { return null; @@ -85,8 +82,7 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper { String[] elements = StringUtils .commaDelimitedListToStringArray(String.valueOf(value)); for (int i = 0; i < elements.length; i++) { - ConfigurationPropertyName name = ConfigurationPropertyName - .of(rootName.toString() + "[" + i + "]"); + ConfigurationPropertyName name = rootName.append("[" + i + "]"); mappings.add(new PropertyMapping(propertySourceName, name, new ElementExtractor(i))); } @@ -106,33 +102,38 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper { return result; } - private String convertName(ConfigurationPropertyName configurationPropertyName) { - return configurationPropertyName.stream() - .map(name -> name.getValue(Form.UNIFORM).toUpperCase()) - .collect(Collectors.joining("_")); + private String convertName(ConfigurationPropertyName name) { + return convertName(name, name.getNumberOfElements()); + } + + private String convertName(ConfigurationPropertyName name, int numberOfElements) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < numberOfElements; i++) { + result.append(result.length() == 0 ? "" : "_"); + result.append(name.getElement(i, Form.UNIFORM).toString().toUpperCase()); + } + return result.toString(); } private boolean isListShortcutPossible(ConfigurationPropertyName name) { - return (name.getElement().isIndexed() - && isNumber(name.getElement().getValue(Form.UNIFORM)) - && name.getParent() != null); + return (name.isLastElementIndexed() && isNumber(name.getLastElement(Form.UNIFORM)) + && name.getNumberOfElements() >= 1); } 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))); + ConfigurationPropertyName name) { + String result = convertName(name, name.getNumberOfElements() - 1) + "__"; + if (propertySource.containsProperty(result)) { + int index = Integer.parseInt(name.getLastElement(Form.UNIFORM)); + return Collections.singletonList( + new PropertyMapping(result, name, new ElementExtractor(index))); } return Collections.emptyList(); } - private String createElement(String value) { - value = value.toLowerCase(); - return (isNumber(value) ? "[" + value + "]" : value); + private CharSequence processElementValue(CharSequence value) { + String result = value.toString().toLowerCase(); + return (isNumber(result) ? "[" + result + "]" : result); } private static boolean isNumber(String string) { 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 415a516238..daff831ecd 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 @@ -58,6 +58,7 @@ public class FileEncodingApplicationListenerTests { Assume.assumeNotNull(System.getProperty("file.encoding")); TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, "spring.mandatory_file_encoding:" + System.getProperty("file.encoding")); + ConfigurationPropertySources.attach(this.environment); this.initializer.onApplicationEvent(this.event); } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java index 971d227446..a022a66c12 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -129,7 +129,6 @@ public class MapBinderTests { 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); 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 deleted file mode 100644 index cbbf99cff8..0000000000 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilderTests.java +++ /dev/null @@ -1,116 +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.context.properties.source; - -import java.util.ArrayList; -import java.util.Arrays; -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.ConfigurationPropertyNameBuilder.ElementValueProcessor; - -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 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", '.'); - assertThat(name.toString()).isEqualTo(expected.toString()); - } - - @Test - public void buildShouldValidateProcessor() { - this.builder = new ConfigurationPropertyNameBuilder( - ElementValueProcessor.identity().withValidName()); - this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("Element value 'foo@!' is not valid"); - this.builder.from("foo@!.bar", '.'); - } - - @Test - public void buildShouldUseElementProcessor() throws Exception { - this.builder = new ConfigurationPropertyNameBuilder( - value -> value.replace("-", "")); - ConfigurationPropertyName name = this.builder.from("FOO_THE-BAR", '_'); - assertThat(name.toString()).isEqualTo("foo.thebar"); - } - - @Test - public void fromNameShouldSetElements() throws Exception { - this.builder = new ConfigurationPropertyNameBuilder(); - ConfigurationPropertyName name = this.builder.from("foo.bar", '.'); - 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 appendShouldAppendElement() throws Exception { - this.builder = new ConfigurationPropertyNameBuilder(); - ConfigurationPropertyName parent = this.builder.from("foo.bar", '.'); - ConfigurationPropertyName name = this.builder.from(parent, "baz"); - assertThat(name.toString()).isEqualTo("foo.bar.baz"); - } - - private List elements(String... elements) { - return Arrays.stream(elements).map(Element::new).collect(Collectors.toList()); - } - - private List getElements(String name) { - List elements = new ArrayList<>(); - for (Element element : this.builder.from(name, '.')) { - elements.add(element); - } - return 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 index 80a515071e..101ae06b72 100644 --- 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 @@ -18,7 +18,6 @@ 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; @@ -26,7 +25,6 @@ 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; @@ -48,7 +46,7 @@ public class ConfigurationPropertyNameTests { public void ofNameShouldNotBeNull() throws Exception { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("Name must not be null"); - ConfigurationPropertyName.of((String) null); + ConfigurationPropertyName.of(null); } @Test @@ -68,14 +66,14 @@ public class ConfigurationPropertyNameTests { @Test public void ofNameShouldNotStartWithDot() throws Exception { this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("Name must not start with '.'"); + this.thrown.expectMessage("is not valid"); ConfigurationPropertyName.of(".foo"); } @Test public void ofNameShouldNotEndWithDot() throws Exception { this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("Name must not end with '.'"); + this.thrown.expectMessage("is not valid"); ConfigurationPropertyName.of("foo."); } @@ -104,48 +102,63 @@ public class ConfigurationPropertyNameTests { 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"); + assertThat(name.getNumberOfElements()).isEqualTo(1); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("name"); + assertThat(name.isIndexed(0)).isFalse(); } @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]"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo"); + assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("bar"); + assertThat(name.isIndexed(0)).isFalse(); + assertThat(name.isIndexed(1)).isTrue(); } @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"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo"); + assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("bar"); + assertThat(name.isIndexed(0)).isFalse(); + assertThat(name.isIndexed(1)).isFalse(); } @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"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo"); + assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("bar"); + assertThat(name.isIndexed(0)).isFalse(); + assertThat(name.isIndexed(1)).isTrue(); } @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"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo"); + assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("bar"); + assertThat(name.getElement(2, Form.ORIGINAL)).isEqualTo("baz"); + assertThat(name.isIndexed(0)).isFalse(); + assertThat(name.isIndexed(1)).isTrue(); + assertThat(name.isIndexed(2)).isFalse(); } @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"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo"); + assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("bar"); + assertThat(name.getElement(2, Form.ORIGINAL)).isEqualTo("baz"); + assertThat(name.isIndexed(0)).isFalse(); + assertThat(name.isIndexed(1)).isTrue(); + assertThat(name.isIndexed(2)).isFalse(); } @Test @@ -173,115 +186,207 @@ public class ConfigurationPropertyNameTests { 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"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo"); + assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("b a r"); + assertThat(name.isIndexed(0)).isFalse(); + assertThat(name.isIndexed(1)).isTrue(); } @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"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo"); + assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("BAR"); + assertThat(name.isIndexed(0)).isFalse(); + assertThat(name.isIndexed(1)).isTrue(); } @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); + public void ofWhenNameIsEmptyShouldReturnEmptyName() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of(""); + assertThat(name.toString()).isEqualTo(""); + assertThat(name.append("foo").toString()).isEqualTo("foo"); } @Test - public void elementNameShouldNotIncludeAngleBrackets() throws Exception { - ConfigurationPropertyName name = ConfigurationPropertyName.of("[foo]"); - assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("foo"); + public void adaptWhenNameIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be null"); + ConfigurationPropertyName.adapt(null, '.'); } @Test - public void elementNameShouldNotIncludeDashes() throws Exception { - ConfigurationPropertyName name = ConfigurationPropertyName.of("f-o-o"); - assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("foo"); + public void adaptWhenElementValueProcessorIsNullShouldThrowException() + throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ElementValueProcessor must not be null"); + ConfigurationPropertyName.adapt("foo", '.', null); } @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(Element::toString) - .iterator(); + public void adaptShouldCreateName() throws Exception { + ConfigurationPropertyName expected = ConfigurationPropertyName.of("foo.bar.baz"); + ConfigurationPropertyName name = ConfigurationPropertyName.adapt("foo.bar.baz", + '.'); + assertThat(name).isEqualTo(expected); } @Test - public void elementIsIndexedWhenIndexedShouldReturnTrue() throws Exception { - assertThat(ConfigurationPropertyName.of("foo[0]").getElement().isIndexed()) + public void adapShouldStripInvalidChars() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.adapt("f@@.b%r", '.'); + assertThat(name.getElement(0, Form.UNIFORM)).isEqualTo("f"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("f"); + assertThat(name.getElement(1, Form.UNIFORM)).isEqualTo("br"); + assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("br"); + assertThat(name.toString()).isEqualTo("f.br"); + } + + @Test + public void adaptShouldSupportUnderscore() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.adapt("f-_o.b_r", '.'); + assertThat(name.getElement(0, Form.UNIFORM)).isEqualTo("fo"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("f-_o"); + assertThat(name.getElement(1, Form.UNIFORM)).isEqualTo("br"); + assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("b_r"); + assertThat(name.toString()).isEqualTo("f-o.br"); + } + + @Test + public void adaptShouldSupportMixedCase() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.adapt("fOo.bAr", '.'); + assertThat(name.getElement(0, Form.UNIFORM)).isEqualTo("foo"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("fOo"); + assertThat(name.getElement(1, Form.UNIFORM)).isEqualTo("bar"); + assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("bAr"); + assertThat(name.toString()).isEqualTo("foo.bar"); + } + + @Test + public void adaptShouldUseElementValueProcessor() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.adapt("FOO_THE-BAR", + '_', (c) -> c.toString().replace("-", "")); + assertThat(name.toString()).isEqualTo("foo.thebar"); + } + + @Test + public void adaptShouldSupportIndexedElements() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.adapt("foo", '.'); + assertThat(name.toString()).isEqualTo("foo"); + assertThat(name.getNumberOfElements()).isEqualTo(1); + name = ConfigurationPropertyName.adapt("[foo]", '.'); + assertThat(name.toString()).isEqualTo("[foo]"); + assertThat(name.getNumberOfElements()).isEqualTo(1); + name = ConfigurationPropertyName.adapt("foo.bar", '.'); + assertThat(name.toString()).isEqualTo("foo.bar"); + assertThat(name.getNumberOfElements()).isEqualTo(2); + name = ConfigurationPropertyName.adapt("foo[foo.bar]", '.'); + assertThat(name.toString()).isEqualTo("foo[foo.bar]"); + assertThat(name.getNumberOfElements()).isEqualTo(2); + name = ConfigurationPropertyName.adapt("foo.[bar].baz", '.'); + assertThat(name.toString()).isEqualTo("foo[bar].baz"); + assertThat(name.getNumberOfElements()).isEqualTo(3); + } + + @Test + public void isEmptyWhenEmptyShouldReturnTrue() throws Exception { + assertThat(ConfigurationPropertyName.of("").isEmpty()).isTrue(); + } + + @Test + public void isEmptyWhenNotEmptyShouldReturnFalse() throws Exception { + assertThat(ConfigurationPropertyName.of("x").isEmpty()).isFalse(); + } + + @Test + public void isLastElementIndexedWhenIndexedShouldReturnTrue() throws Exception { + assertThat(ConfigurationPropertyName.of("foo[0]").isLastElementIndexed()) .isTrue(); } @Test - public void elementIsIndexedWhenNotIndexedShouldReturnFalse() throws Exception { - assertThat(ConfigurationPropertyName.of("foo.bar").getElement().isIndexed()) + public void isLastElementIndexedWhenNotIndexedShouldReturnFalse() throws Exception { + assertThat(ConfigurationPropertyName.of("foo.bar").isLastElementIndexed()) + .isFalse(); + assertThat(ConfigurationPropertyName.of("foo[0].bar").isLastElementIndexed()) .isFalse(); } @Test - public void isAncestorOfWhenSameShouldReturnFalse() throws Exception { - ConfigurationPropertyName parent = ConfigurationPropertyName.of("foo"); - assertThat(parent.isAncestorOf(parent)).isFalse(); + public void getLastElementShouldGetLastElement() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.adapt("foo.bAr", '.'); + assertThat(name.getLastElement(Form.ORIGINAL)).isEqualTo("bAr"); + assertThat(name.getLastElement(Form.UNIFORM)).isEqualTo("bar"); } @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(); + public void getLastElementWhenEmptyShouldReturnEmptyString() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.EMPTY; + assertThat(name.getLastElement(Form.ORIGINAL)).isEqualTo(""); + assertThat(name.getLastElement(Form.UNIFORM)).isEqualTo(""); } @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(); + public void getElementShouldNotIncludeAngleBrackets() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("[foo]"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo"); + assertThat(name.getElement(0, Form.UNIFORM)).isEqualTo("foo"); } @Test - public void isAncestorOfWhenRootReturnTrue() throws Exception { - ConfigurationPropertyName parent = ConfigurationPropertyName.of(""); - ConfigurationPropertyName grandchild = ConfigurationPropertyName - .of("foo.bar.baz"); - assertThat(parent.isAncestorOf(grandchild)).isTrue(); - assertThat(grandchild.isAncestorOf(parent)).isFalse(); + public void getElementInUniformFormShouldNotIncludeDashes() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("f-o-o"); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("f-o-o"); + assertThat(name.getElement(0, Form.UNIFORM)).isEqualTo("foo"); + } + + @Test + public void getElementInOriginalFormShouldReturnElement() throws Exception { + assertThat(getElements("foo.bar", Form.ORIGINAL)).containsExactly("foo", "bar"); + assertThat(getElements("foo[0]", Form.ORIGINAL)).containsExactly("foo", "0"); + assertThat(getElements("foo.[0]", Form.ORIGINAL)).containsExactly("foo", "0"); + assertThat(getElements("foo[baz]", Form.ORIGINAL)).containsExactly("foo", "baz"); + assertThat(getElements("foo.baz", Form.ORIGINAL)).containsExactly("foo", "baz"); + assertThat(getElements("foo[baz].bar", Form.ORIGINAL)).containsExactly("foo", + "baz", "bar"); + assertThat(getElements("foo.baz.bar", Form.ORIGINAL)).containsExactly("foo", + "baz", "bar"); + assertThat(getElements("foo.baz-bar", Form.ORIGINAL)).containsExactly("foo", + "baz-bar"); + } + + @Test + public void getElementInUniformFormShouldReturnElement() throws Exception { + assertThat(getElements("foo.bar", Form.UNIFORM)).containsExactly("foo", "bar"); + assertThat(getElements("foo[0]", Form.UNIFORM)).containsExactly("foo", "0"); + assertThat(getElements("foo.[0]", Form.UNIFORM)).containsExactly("foo", "0"); + assertThat(getElements("foo[baz]", Form.UNIFORM)).containsExactly("foo", "baz"); + assertThat(getElements("foo.baz", Form.UNIFORM)).containsExactly("foo", "baz"); + assertThat(getElements("foo[baz].bar", Form.UNIFORM)).containsExactly("foo", + "baz", "bar"); + assertThat(getElements("foo.baz.bar", Form.UNIFORM)).containsExactly("foo", "baz", + "bar"); + assertThat(getElements("foo.baz-bar", Form.UNIFORM)).containsExactly("foo", + "bazbar"); + } + + private List getElements(String name, Form form) { + ConfigurationPropertyName propertyName = ConfigurationPropertyName.of(name); + List result = new ArrayList<>(propertyName.getNumberOfElements()); + for (int i = 0; i < propertyName.getNumberOfElements(); i++) { + result.add(propertyName.getElement(i, form)); + } + return result; + } + + @Test + public void getNumberOfElementsShouldReturnNumberOfElement() throws Exception { + assertThat(ConfigurationPropertyName.of("").getNumberOfElements()).isEqualTo(0); + assertThat(ConfigurationPropertyName.of("x").getNumberOfElements()).isEqualTo(1); + assertThat(ConfigurationPropertyName.of("x.y").getNumberOfElements()) + .isEqualTo(2); + assertThat(ConfigurationPropertyName.of("x[0].y").getNumberOfElements()) + .isEqualTo(3); } @Test @@ -294,7 +399,7 @@ public class ConfigurationPropertyNameTests { public void appendWhenIndexedShouldAppendWithBrackets() throws Exception { ConfigurationPropertyName name = ConfigurationPropertyName.of("foo") .append("[bar]"); - assertThat(name.getElement().isIndexed()).isTrue(); + assertThat(name.isLastElementIndexed()).isTrue(); assertThat(name.toString()).isEqualTo("foo[bar]"); } @@ -305,12 +410,103 @@ public class ConfigurationPropertyNameTests { ConfigurationPropertyName.of("foo").append("1bar"); } + @Test + public void appendWhenElementNameMultiDotShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Element value 'bar.baz' must be a single item"); + ConfigurationPropertyName.of("foo").append("bar.baz"); + } + @Test public void appendWhenElementNameIsNullShouldReturnName() throws Exception { ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); assertThat((Object) name.append((String) null)).isSameAs(name); } + @Test + public void chopWhenLessThenSizeShouldReturnChopped() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar.baz"); + assertThat(name.chop(1).toString()).isEqualTo("foo"); + assertThat(name.chop(2).toString()).isEqualTo("foo.bar"); + } + + @Test + public void chopWhenGreaterThanSizeShouldReturnExisting() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar.baz"); + assertThat(name.chop(4)).isEqualTo(name); + } + + @Test + public void chopWhenEqualToSizeShouldReturnExisting() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar.baz"); + assertThat(name.chop(3)).isEqualTo(name); + } + + @Test + public void isParentOfWhenSameShouldReturnFalse() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + assertThat(name.isParentOf(name)).isFalse(); + } + + @Test + public void isParentOfWhenParentShouldReturnTrue() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + ConfigurationPropertyName child = ConfigurationPropertyName.of("foo.bar"); + assertThat(name.isParentOf(child)).isTrue(); + assertThat(child.isParentOf(name)).isFalse(); + } + + @Test + public void isParentOfWhenGrandparentShouldReturnFalse() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + ConfigurationPropertyName grandchild = ConfigurationPropertyName + .of("foo.bar.baz"); + assertThat(name.isParentOf(grandchild)).isFalse(); + assertThat(grandchild.isParentOf(name)).isFalse(); + } + + @Test + public void isParentOfWhenRootReturnTrue() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of(""); + ConfigurationPropertyName child = ConfigurationPropertyName.of("foo"); + ConfigurationPropertyName grandchild = ConfigurationPropertyName.of("foo.bar"); + assertThat(name.isParentOf(child)).isTrue(); + assertThat(name.isParentOf(grandchild)).isFalse(); + assertThat(child.isAncestorOf(name)).isFalse(); + } + + @Test + public void isAncestorOfWhenSameShouldReturnFalse() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + assertThat(name.isAncestorOf(name)).isFalse(); + } + + @Test + public void isAncestorOfWhenParentShouldReturnTrue() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + ConfigurationPropertyName child = ConfigurationPropertyName.of("foo.bar"); + assertThat(name.isAncestorOf(child)).isTrue(); + assertThat(child.isAncestorOf(name)).isFalse(); + } + + @Test + public void isAncestorOfWhenGrandparentShouldReturnTrue() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + ConfigurationPropertyName grandchild = ConfigurationPropertyName + .of("foo.bar.baz"); + assertThat(name.isAncestorOf(grandchild)).isTrue(); + assertThat(grandchild.isAncestorOf(name)).isFalse(); + } + + @Test + public void isAncestorOfWhenRootShouldReturnTrue() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of(""); + ConfigurationPropertyName grandchild = ConfigurationPropertyName + .of("foo.bar.baz"); + assertThat(name.isAncestorOf(grandchild)).isTrue(); + assertThat(grandchild.isAncestorOf(name)).isFalse(); + } + @Test public void compareShouldSortNames() throws Exception { List names = new ArrayList<>(); @@ -327,10 +523,41 @@ public class ConfigurationPropertyNameTests { } @Test - public void ofNameCanBeEmpty() throws Exception { - ConfigurationPropertyName name = ConfigurationPropertyName.of(""); - assertThat(name.toString()).isEqualTo(""); - assertThat(name.append("foo").toString()).isEqualTo("foo"); + public void toStringShouldBeLowerCaseDashed() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.adapt("fOO.b_-a-r", + '.'); + assertThat(name.toString()).isEqualTo("foo.b-a-r"); + } + + @Test + public void equalsAndHashCode() throws Exception { + ConfigurationPropertyName n01 = ConfigurationPropertyName.of("foo[bar]"); + ConfigurationPropertyName n02 = ConfigurationPropertyName.of("foo[bar]"); + ConfigurationPropertyName n03 = ConfigurationPropertyName.of("foo.bar"); + ConfigurationPropertyName n04 = ConfigurationPropertyName.of("f-o-o.b-a-r"); + ConfigurationPropertyName n05 = ConfigurationPropertyName.of("foo[BAR]"); + ConfigurationPropertyName n06 = ConfigurationPropertyName.of("oof[bar]"); + ConfigurationPropertyName n07 = ConfigurationPropertyName.of("foo.bar"); + ConfigurationPropertyName n08 = ConfigurationPropertyName.EMPTY; + ConfigurationPropertyName n09 = ConfigurationPropertyName.of("foo"); + ConfigurationPropertyName n10 = ConfigurationPropertyName.of("fo"); + ConfigurationPropertyName n11 = ConfigurationPropertyName.adapt("foo.BaR", '.'); + assertThat(n01.hashCode()).isEqualTo(n02.hashCode()); + assertThat(n01.hashCode()).isEqualTo(n02.hashCode()); + assertThat(n01.hashCode()).isEqualTo(n03.hashCode()); + assertThat(n01.hashCode()).isEqualTo(n04.hashCode()); + assertThat(n01.hashCode()).isEqualTo(n11.hashCode()); + assertThat((Object) n01).isEqualTo(n01); + assertThat((Object) n01).isEqualTo(n02); + assertThat((Object) n01).isEqualTo(n03); + assertThat((Object) n01).isEqualTo(n04); + assertThat((Object) n11).isEqualTo(n03); + assertThat((Object) n03).isEqualTo(n11); + assertThat((Object) n01).isNotEqualTo(n05); + assertThat((Object) n01).isNotEqualTo(n06); + assertThat((Object) n07).isNotEqualTo(n08); + assertThat((Object) n09).isNotEqualTo(n10); + assertThat((Object) n10).isNotEqualTo(n09); } @Test