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