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
pull/9145/head
Phillip Webb 8 years ago
parent d969ebad07
commit 961d41f6f6

@ -84,22 +84,6 @@ abstract class AggregateBinder<T> {
return this.context; 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. * Internal class used to supply the aggregate and cache the value.
* @param <T> The aggregate type * @param <T> The aggregate type

@ -42,6 +42,8 @@ import org.springframework.util.MultiValueMap;
*/ */
abstract class IndexedElementsBinder<T> extends AggregateBinder<T> { abstract class IndexedElementsBinder<T> extends AggregateBinder<T> {
private static final String INDEX_ZERO = "[0]";
IndexedElementsBinder(BindContext context) { IndexedElementsBinder(BindContext context) {
super(context); super(context);
} }
@ -81,12 +83,13 @@ abstract class IndexedElementsBinder<T> extends AggregateBinder<T> {
MultiValueMap<String, ConfigurationProperty> knownIndexedChildren = getKnownIndexedChildren( MultiValueMap<String, ConfigurationProperty> knownIndexedChildren = getKnownIndexedChildren(
source, root); source, root);
for (int i = 0; i < Integer.MAX_VALUE; i++) { 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); Object value = elementBinder.bind(name, Bindable.of(elementType), source);
if (value == null) { if (value == null) {
break; break;
} }
knownIndexedChildren.remove(name.getElement().getValue(Form.UNIFORM)); knownIndexedChildren.remove(name.getLastElement(Form.UNIFORM));
collection.get().add(value); collection.get().add(value);
} }
assertNoUnboundChildren(knownIndexedChildren); assertNoUnboundChildren(knownIndexedChildren);
@ -100,9 +103,9 @@ abstract class IndexedElementsBinder<T> extends AggregateBinder<T> {
} }
for (ConfigurationPropertyName name : (IterableConfigurationPropertySource) source for (ConfigurationPropertyName name : (IterableConfigurationPropertySource) source
.filter(root::isAncestorOf)) { .filter(root::isAncestorOf)) {
name = rollUp(name, root); name = name.chop(root.getNumberOfElements() + 1);
if (name.getElement().isIndexed()) { if (name.isLastElementIndexed()) {
String key = name.getElement().getValue(Form.UNIFORM); String key = name.getLastElement(Form.UNIFORM);
ConfigurationProperty value = source.getConfigurationProperty(name); ConfigurationProperty value = source.getConfigurationProperty(name);
children.add(key, value); children.add(key, value);
} }

@ -18,7 +18,6 @@ package org.springframework.boot.context.properties.bind;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.boot.context.properties.bind.convert.BinderConversionService; import org.springframework.boot.context.properties.bind.convert.BinderConversionService;
import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationProperty;
@ -97,7 +96,7 @@ class MapBinder extends AggregateBinder<Map<Object, Object>> {
} }
private Bindable<?> getValueBindable(ConfigurationPropertyName name) { private Bindable<?> getValueBindable(ConfigurationPropertyName name) {
if (isMultiElementName(name) && isValueTreatedAsNestedMap()) { if (!this.root.isParentOf(name) && isValueTreatedAsNestedMap()) {
return Bindable.of(this.mapType); return Bindable.of(this.mapType);
} }
return Bindable.of(this.valueType); return Bindable.of(this.valueType);
@ -105,17 +104,13 @@ class MapBinder extends AggregateBinder<Map<Object, Object>> {
private ConfigurationPropertyName getEntryName(ConfigurationPropertySource source, private ConfigurationPropertyName getEntryName(ConfigurationPropertySource source,
ConfigurationPropertyName name) { ConfigurationPropertyName name) {
if (isMultiElementName(name) if (!this.root.isParentOf(name)
&& (isValueTreatedAsNestedMap() || !isScalarValue(source, name))) { && (isValueTreatedAsNestedMap() || !isScalarValue(source, name))) {
return rollUp(name, this.root); return name.chop(this.root.getNumberOfElements() + 1);
} }
return name; return name;
} }
private boolean isMultiElementName(ConfigurationPropertyName name) {
return name.getParent() != null && !this.root.equals(name.getParent());
}
private boolean isValueTreatedAsNestedMap() { private boolean isValueTreatedAsNestedMap() {
return Object.class.equals(this.valueType.resolve(Object.class)); return Object.class.equals(this.valueType.resolve(Object.class));
} }
@ -139,8 +134,13 @@ class MapBinder extends AggregateBinder<Map<Object, Object>> {
} }
private String getKeyName(ConfigurationPropertyName name) { private String getKeyName(ConfigurationPropertyName name) {
return name.stream(this.root).map((e) -> e.getValue(Form.ORIGINAL)) StringBuilder result = new StringBuilder();
.collect(Collectors.joining(".")); 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();
} }
} }

@ -22,8 +22,6 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.assertj.core.util.Objects;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
@ -97,8 +95,9 @@ public class ValidationErrors implements Iterable<ObjectError> {
private boolean isForError(ConfigurationPropertyName name, private boolean isForError(ConfigurationPropertyName name,
ConfigurationPropertyName boundPropertyName, FieldError error) { ConfigurationPropertyName boundPropertyName, FieldError error) {
return Objects.areEqual(boundPropertyName.getParent(), name) && boundPropertyName return name.isParentOf(boundPropertyName)
.getElement().getValue(Form.UNIFORM).equalsIgnoreCase(error.getField()); && boundPropertyName.getLastElement(Form.UNIFORM).toString()
.equalsIgnoreCase(error.getField());
} }
/** /**

@ -16,23 +16,20 @@
package org.springframework.boot.context.properties.source; package org.springframework.boot.context.properties.source;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern; import java.util.function.Function;
import java.util.stream.Stream;
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.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/** /**
* A configuration property name composed of elements separated by dots. Names may contain * A configuration property name composed of elements separated by dots. User created
* the characters "{@code a-z}" "{@code 0-9}") and "{@code -}", they must be lower-case * names may contain the characters "{@code a-z}" "{@code 0-9}") and "{@code -}", they
* and must start with a letter. The "{@code -}" is used purely for formatting, i.e. * must be lower-case and must start with a letter. The "{@code -}" is used purely for
* "{@code foo-bar}" and "{@code foobar}" are considered equivalent. * formatting, i.e. "{@code foo-bar}" and "{@code foobar}" are considered equivalent.
* <p> * <p>
* The "{@code [}" and "{@code ]}" characters may be used to indicate an associative * 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 * 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;
* <li>{@code server.hosts[0].name}</li> * <li>{@code server.hosts[0].name}</li>
* <li>{@code log[org.springboot].level}</li> * <li>{@code log[org.springboot].level}</li>
* </ul> * </ul>
* <p>
* *
* @author Phillip Webb * @author Phillip Webb
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.0.0 * @since 2.0.0
* @see #of(String) * @see #of(CharSequence)
* @see ConfigurationPropertyNameBuilder
* @see ConfigurationPropertySource * @see ConfigurationPropertySource
*/ */
public final class ConfigurationPropertyName public final class ConfigurationPropertyName
implements Iterable<Element>, Comparable<ConfigurationPropertyName> { implements Comparable<ConfigurationPropertyName> {
private static final String EMPTY_STRING = "";
/** /**
* An empty {@link ConfigurationPropertyName}. * An empty {@link ConfigurationPropertyName}.
*/ */
public static final ConfigurationPropertyName EMPTY = new ConfigurationPropertyName( public static final ConfigurationPropertyName EMPTY = new ConfigurationPropertyName(
null, new Element()); new String[0]);
private static final ConfigurationPropertyNameBuilder BUILDER = new ConfigurationPropertyNameBuilder( private final CharSequence[] elements;
ElementValueProcessor.identity().withValidName());
private final ConfigurationPropertyName parent; private final CharSequence[] uniformElements;
private final Element element; private int[] elementHashCodes;
private String toString; private String string;
private ConfigurationPropertyName(CharSequence[] elements) {
this(elements, new CharSequence[elements.length]);
}
ConfigurationPropertyName(ConfigurationPropertyName parent, Element element) { private ConfigurationPropertyName(CharSequence[] elements,
Assert.notNull(element, "Element must not be null"); CharSequence[] uniformElements) {
this.parent = (parent == EMPTY ? null : parent); this.elements = elements;
this.element = element; this.uniformElements = uniformElements;
} }
/** /**
* Return the parent of this configuration property. * Returns {@code true} if this {@link ConfigurationPropertyName} is empty.
* @return the parent or {code null} * @return {@code true} if the name is empty
*/ */
public ConfigurationPropertyName getParent() { public boolean isEmpty() {
return this.parent; return this.elements.length == 0;
} }
/** /**
* Return the element part of this configuration property name. * Return if the last element in the name is indexed.
* @return the element (never {@code null}) * @return {@code true} if the last element is indexed
*/ */
public Element getElement() { public boolean isLastElementIndexed() {
return this.element; int size = getNumberOfElements();
return (size > 0 && isIndexed(this.elements[size - 1]));
} }
@Override /**
public Iterator<Element> iterator() { * Return if the an element in the name is indexed.
return stream().iterator(); * @param elementIndex the index of the element
* @return {@code true} if the last element is indexed
*/
boolean isIndexed(int elementIndex) {
return isIndexed(this.elements[elementIndex]);
} }
/** /**
* Return a stream of the {@link Element Elements} that make up this name. * Return the last element in the name in the given form.
* @return a stream of {@link Element} items * @param form the form to return
* @return the last element
*/ */
public Stream<Element> stream() { public String getLastElement(Form form) {
if (this.parent == null) { int size = getNumberOfElements();
return Stream.of(this.element); 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)); 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 result.toString();
}
/**
* Return the total number of elements in the name.
* @return the number of elements
*/
public int getNumberOfElements() {
return this.elements.length;
} }
/** /**
* Return a stream of the {@link Element Elements} that make up this name starting * Create a new {@link ConfigurationPropertyName} by appending the given element
* from the given root. * value.
* @param root the root of the name or {@code null} to stream all elements * @param elementValue the single element value to append
* @return a stream of {@link Element} items * @return a new {@link ConfigurationPropertyName}
*/ */
public Stream<Element> stream(ConfigurationPropertyName root) { public ConfigurationPropertyName append(String elementValue) {
if (this.parent == null || this.parent.equals(root)) { if (elementValue == null) {
return Stream.of(this.element); return this;
} }
return Stream.concat(this.parent.stream(root), Stream.of(this.element)); 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);
} }
@Override /**
public String toString() { * Return a new {@link ConfigurationPropertyName} by chopping this name to the given
if (this.toString == null) { * {@code size}. For example, {@code chop(1)} on the name {@code foo.bar} will return
this.toString = buildToString(); * {@code foo}.
* @param size the size to chop
* @return the chopped name
*/
public ConfigurationPropertyName chop(int size) {
if (size >= getNumberOfElements()) {
return this;
} }
return this.toString; 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);
} }
private String buildToString() { /**
StringBuilder result = new StringBuilder(); * Returns {@code true} if this element is an immediate parent of the specified name.
result.append(this.parent != null ? this.parent.toString() : ""); * @param name the name to check
result.append(result.length() > 0 && !this.element.isIndexed() ? "." : ""); * @return {@code true} if this name is an ancestor
result.append(this.element); */
return result.toString(); 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) or * Returns {@code true} if this element is an ancestor (immediate or nested parent) of
* the specified name. * the specified name.
* @param name the name to check * @param name the name to check
* @return {@code true} if this name is an ancestor * @return {@code true} if this name is an ancestor
*/ */
public boolean isAncestorOf(ConfigurationPropertyName name) { public boolean isAncestorOf(ConfigurationPropertyName name) {
if (this.equals(EMPTY)) { Assert.notNull(name, "Name must not be null");
return true; if (this.getNumberOfElements() >= name.getNumberOfElements()) {
return false;
} }
ConfigurationPropertyName candidate = (name == null ? null : name.getParent()); for (int i = 0; i < this.elements.length; i++) {
while (candidate != null) { if (!elementEquals(this.elements[i], name.elements[i])) {
if (candidate.equals(this)) { return false;
return true;
} }
candidate = candidate.getParent();
} }
return false; return true;
} }
@Override @Override
public int compareTo(ConfigurationPropertyName other) { public int compareTo(ConfigurationPropertyName other) {
Iterator<Element> elements = iterator(); return compare(this, other);
Iterator<Element> otherElements = other.iterator(); }
while (elements.hasNext() || otherElements.hasNext()) {
int result = compare(elements.hasNext() ? elements.next() : null, private int compare(ConfigurationPropertyName n1, ConfigurationPropertyName n2) {
otherElements.hasNext() ? otherElements.next() : null); 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) { if (result != 0) {
return result; return result;
} }
@ -172,97 +248,169 @@ public final class ConfigurationPropertyName
return 0; return 0;
} }
private int compare(Element element, Element other) { private int compare(String e1, boolean indexed1, String e2, boolean indexed2) {
if (element == null) { if (e1 == null) {
return -1; return -1;
} }
if (other == null) { if (e2 == null) {
return 1; 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 @Override
public int hashCode() { public int hashCode() {
int result = 1; if (this.elementHashCodes == null) {
result = 31 * result + ObjectUtils.nullSafeHashCode(this.parent); this.elementHashCodes = getElementHashCodes();
result = 31 * result + ObjectUtils.nullSafeHashCode(this.element); }
return result; 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 @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) { if (obj == this) {
return true; return true;
} }
if (obj == null || getClass() != obj.getClass()) { if (obj == null || !obj.getClass().equals(getClass())) {
return false; return false;
} }
ConfigurationPropertyName other = (ConfigurationPropertyName) obj; ConfigurationPropertyName other = (ConfigurationPropertyName) obj;
boolean result = true; if (getNumberOfElements() != other.getNumberOfElements()) {
result = result && ObjectUtils.nullSafeEquals(this.parent, other.parent); return false;
result = result && ObjectUtils.nullSafeEquals(this.element, other.element); }
return result; 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) {
* Create a new {@link ConfigurationPropertyName} by appending the given index. int l1 = e1.length();
* @param index the index to append int l2 = e2.length();
* @return a new {@link ConfigurationPropertyName} boolean indexed1 = isIndexed(e1);
*/ int offset1 = (indexed1 ? 1 : 0);
public ConfigurationPropertyName appendIndex(int index) { boolean indexed2 = isIndexed(e2);
return BUILDER.from(this, index); 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) {
* Create a new {@link ConfigurationPropertyName} by appending the given element. int length = element.length();
* @param element the element to append return length > 2 && element.charAt(0) == '['
* @return a new {@link ConfigurationPropertyName} && element.charAt(length - 1) == ']';
*/
public ConfigurationPropertyName append(String element) {
if (StringUtils.hasLength(element)) {
return BUILDER.from(this, element);
}
return this;
} }
/** /**
* Returns if the given name is valid. If this method returns {@code true} then the * 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 * @param name the name to test
* @return {@code true} if the name is valid * @return {@code true} if the name is valid
*/ */
public static boolean isValid(String name) { public static boolean isValid(CharSequence name) {
if (name == null) { if (name == null) {
return false; return false;
} }
boolean indexed = false; if (name.equals(EMPTY_STRING)) {
int charIndex = 0; return true;
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;
}
}
} }
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 * @return a {@link ConfigurationPropertyName} instance
* @throws IllegalArgumentException if the name is not valid * @throws IllegalArgumentException if the name is not valid
*/ */
public static ConfigurationPropertyName of(String name) public static ConfigurationPropertyName of(CharSequence name) {
throws IllegalArgumentException {
Assert.notNull(name, "Name must not be null"); Assert.notNull(name, "Name must not be null");
Assert.isTrue(!name.startsWith("."), "Name must not start with '.'"); if (name.length() >= 1
Assert.isTrue(!name.endsWith("."), "Name must not end with '.'"); && (name.charAt(0) == '.' || name.charAt(name.length() - 1) == '.')) {
if (StringUtils.isEmpty(name)) { throw new IllegalArgumentException(
"Configuration property name '" + name + "' is not valid");
}
if (name.length() == 0) {
return EMPTY; return EMPTY;
} }
return BUILDER.from(name, '.'); List<CharSequence> elements = new ArrayList<CharSequence>(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<Element> { 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; * Create a {@link ConfigurationPropertyName} by adapting the given source. The name
this.value = Form.expand("", false); * 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.
* <p>
* 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<CharSequence, CharSequence> elementValueProcessor) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(elementValueProcessor, "ElementValueProcessor must not be null");
if (name.length() == 0) {
return EMPTY;
} }
List<CharSequence> elements = new ArrayList<CharSequence>(10);
Element(String value) { process(name, separator, (elementValue, start, end, indexed) -> {
Assert.notNull(value, "Value must not be null"); elementValue = elementValueProcessor.apply(elementValue);
this.indexed = isIndexed(value); if (!isIndexed(elementValue)) {
value = (this.indexed ? value.substring(1, value.length() - 1) : value); elementValue = cleanupCharSequence(elementValue,
if (!this.indexed) { (ch, index) -> ch != '_' && !ElementValidator
validate(value); .isValidChar(Character.toLowerCase(ch), index),
CharProcessor.NONE);
} }
this.value = Form.expand(value, this.indexed); if (elementValue.length() > 0) {
} elements.add(elementValue);
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 (this.indexed && other.indexed) { });
try { return new ConfigurationPropertyName(
long value = Long.parseLong(getValue(Form.UNIFORM)); elements.toArray(new CharSequence[elements.size()]));
long otherValue = Long.parseLong(other.getValue(Form.UNIFORM)); }
return Long.compare(value, otherValue);
}
catch (NumberFormatException ex) {
// Fallback to string comparison
}
}
return getValue(Form.UNIFORM).compareTo(other.getValue(Form.UNIFORM));
}
@Override
public int hashCode() {
return getValue(Form.UNIFORM).hashCode();
}
@Override private static void process(CharSequence name, char separator,
public boolean equals(Object obj) { ElementProcessor processor) {
if (this == obj) { int start = 0;
return true; 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;
} }
if (obj == null || getClass() != obj.getClass()) { else if (!indexed && ch == '[') {
return false; processElement(processor, name, start, i, indexed);
start = i;
indexed = true;
}
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));
}
@Override
public String toString() {
String string = getValue(Form.CONFIGURATION);
return (this.indexed ? "[" + string + "]" : string);
}
/**
* 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;
} }
processElement(processor, name, start, length, false);
}
/** private static void processElement(ElementProcessor processor, CharSequence name,
* Return the element value in the specified form. Indexed values (the part within int start, int end, boolean indexed) {
* square brackets) are always returned unchanged. if ((end - start) >= 1) {
* @param form the form the value should take processor.process(name.subSequence(start, end), start, end, indexed);
* @return the value
*/
public String getValue(Form form) {
form = (form != null ? form : Form.ORIGINAL);
return this.value[form.ordinal()];
} }
}
static boolean isValid(String value) { private static CharSequence cleanupCharSequence(CharSequence name, CharFilter filter,
if (!isIndexed(value)) { CharProcessor processor) {
for (int i = 0; i < value.length(); i++) { for (int i = 0; i < name.length(); i++) {
if (!isValid(i, value.charAt(i))) { char ch = name.charAt(i);
return false; 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 { 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:
* <ul> * <ul>
* <li>"{@code foo-bar}" = "{@code foo-bar}"</li> * <li>"{@code foo-bar}" = "{@code foo-bar}"</li>
* <li>"{@code fooBar}" = "{@code fooBar}"</li> * <li>"{@code fooBar}" = "{@code fooBar}"</li>
@ -414,33 +557,7 @@ public final class ConfigurationPropertyName
* <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li> * <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li>
* </ul> * </ul>
*/ */
ORIGINAL { ORIGINAL,
@Override
protected String convert(String value) {
return value;
}
},
/**
* The canonical configuration form (lower-case with only alphanumeric and
* "{@code -}" characters).
* <ul>
* <li>"{@code foo-bar}" = "{@code foo-bar}"</li>
* <li>"{@code fooBar}" = "{@code foobar}"</li>
* <li>"{@code foo_bar}" = "{@code foobar}"</li>
* <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li>
* </ul>
*/
CONFIGURATION {
@Override
protected boolean isIncluded(char ch) {
return Character.isAlphabetic(ch) || Character.isDigit(ch) || (ch == '-');
}
},
/** /**
* The uniform configuration form (used for equals/hashCode; lower-case with only * The uniform configuration form (used for equals/hashCode; lower-case with only
@ -452,53 +569,77 @@ public final class ConfigurationPropertyName
* <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li> * <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li>
* </ul> * </ul>
*/ */
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);
* 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) { * Internal filter used to strip out characters.
StringBuilder result = new StringBuilder(value.length()); */
for (int i = 0; i < value.length(); i++) { private interface CharFilter {
char ch = value.charAt(i);
if (isIncluded(ch)) { boolean isExcluded(char ch, int index);
result.append(Character.toLowerCase(ch));
} }
/**
* 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);
} }
return result.toString();
} }
/** public boolean isValid() {
* Called to determine of the specified character is valid for the form. return this.valid;
* @param ch the character to test }
* @return if the character is value
*/ public static boolean isValidElement(CharSequence elementValue) {
protected boolean isIncluded(char ch) { for (int i = 0; i < elementValue.length(); i++) {
if (!isValidChar(elementValue.charAt(i), i)) {
return false;
}
}
return true; return true;
} }
/** public static boolean isValidChar(char ch, int index) {
* Expand the given value to all an array containing the value in each boolean isAlpha = ch >= 'a' && ch <= 'z';
* {@link Form}. boolean isNumeric = ch >= '0' && ch <= '9';
* @param value the source value if (index == 0) {
* @param indexed if the value is indexed return isAlpha;
* @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));
} }
return result; return isAlpha || isNumeric || ch == '-';
} }
} }

@ -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<String, ConfigurationPropertyName> 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<Element> 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<Element> elements, StringBuilder value) {
if (value.length() > 0) {
elements.add(buildElement(value.toString()));
value.setLength(0);
}
}
private ConfigurationPropertyName from(Iterator<Element> 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;
};
}
}
}

@ -40,8 +40,6 @@ final class DefaultPropertyMapper implements PropertyMapper {
private LastMapping<String> lastMappedPropertyName; private LastMapping<String> lastMappedPropertyName;
private final ConfigurationPropertyNameBuilder nameBuilder = new ConfigurationPropertyNameBuilder();
private DefaultPropertyMapper() { private DefaultPropertyMapper() {
} }
@ -76,14 +74,17 @@ final class DefaultPropertyMapper implements PropertyMapper {
private List<PropertyMapping> tryMap(String propertySourceName) { private List<PropertyMapping> tryMap(String propertySourceName) {
try { try {
ConfigurationPropertyName convertedName = this.nameBuilder ConfigurationPropertyName convertedName = ConfigurationPropertyName
.from(propertySourceName, '.'); .adapt(propertySourceName, '.');
PropertyMapping o = new PropertyMapping(propertySourceName, convertedName); if (!convertedName.isEmpty()) {
return Collections.singletonList(o); PropertyMapping o = new PropertyMapping(propertySourceName,
convertedName);
return Collections.singletonList(o);
}
} }
catch (Exception ex) { catch (Exception ex) {
return Collections.emptyList();
} }
return Collections.emptyList();
} }
private static class LastMapping<T> { private static class LastMapping<T> {

@ -48,7 +48,8 @@ class FilteredConfigurationPropertiesSource implements ConfigurationPropertySour
} }
@Override @Override
public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { public ConfigurationPropertyState containsDescendantOf(
ConfigurationPropertyName name) {
ConfigurationPropertyState result = this.source.containsDescendantOf(name); ConfigurationPropertyState result = this.source.containsDescendantOf(name);
if (result == ConfigurationPropertyState.PRESENT) { if (result == ConfigurationPropertyState.PRESENT) {
// We can't be sure a contained descendant won't be filtered // We can't be sure a contained descendant won't be filtered

@ -47,7 +47,8 @@ class FilteredIterableConfigurationPropertiesSource
} }
@Override @Override
public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { public ConfigurationPropertyState containsDescendantOf(
ConfigurationPropertyName name) {
return ConfigurationPropertyState.search(this, name::isAncestorOf); return ConfigurationPropertyState.search(this, name::isAncestorOf);
} }

@ -61,7 +61,8 @@ public interface IterableConfigurationPropertySource
Stream<ConfigurationPropertyName> stream(); Stream<ConfigurationPropertyName> stream();
@Override @Override
default ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { default ConfigurationPropertyState containsDescendantOf(
ConfigurationPropertyName name) {
return ConfigurationPropertyState.search(this, name::isAncestorOf); return ConfigurationPropertyState.search(this, name::isAncestorOf);
} }

@ -27,10 +27,10 @@ import org.springframework.core.env.PropertySource;
* <P> * <P>
* Mappings should be provided for both {@link ConfigurationPropertyName * Mappings should be provided for both {@link ConfigurationPropertyName
* ConfigurationPropertyName} types and {@code String} based names. This allows the * ConfigurationPropertyName} types and {@code String} based names. This allows the
* {@link SpringConfigurationPropertySource} to first attempt any direct mappings * {@link SpringConfigurationPropertySource} to first attempt any direct mappings (i.e.
* (i.e. map the {@link ConfigurationPropertyName} directly to the {@link PropertySource} * map the {@link ConfigurationPropertyName} directly to the {@link PropertySource} name)
* name) before falling back to {@link EnumerablePropertySource enumerating} property * before falling back to {@link EnumerablePropertySource enumerating} property names,
* names, mapping them to a {@link ConfigurationPropertyName} and checking for * mapping them to a {@link ConfigurationPropertyName} and checking for
* {@link PropertyMapping#isApplicable(ConfigurationPropertyName) applicability}. See * {@link PropertyMapping#isApplicable(ConfigurationPropertyName) applicability}. See
* {@link SpringConfigurationPropertySource} for more details. * {@link SpringConfigurationPropertySource} for more details.
* *

@ -89,7 +89,8 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource {
} }
@Override @Override
public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { public ConfigurationPropertyState containsDescendantOf(
ConfigurationPropertyName name) {
return this.containsDescendantOfMethod.apply(name); return this.containsDescendantOfMethod.apply(name);
} }
@ -176,7 +177,8 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource {
PropertySource<?> source) { PropertySource<?> source) {
if (source instanceof RandomValuePropertySource) { if (source instanceof RandomValuePropertySource) {
return (name) -> (name.isAncestorOf(RANDOM) || name.equals(RANDOM) return (name) -> (name.isAncestorOf(RANDOM) || name.equals(RANDOM)
? ConfigurationPropertyState.PRESENT : ConfigurationPropertyState.ABSENT); ? ConfigurationPropertyState.PRESENT
: ConfigurationPropertyState.ABSENT);
} }
return null; return null;
} }

@ -87,7 +87,8 @@ class SpringIterableConfigurationPropertySource extends SpringConfigurationPrope
} }
@Override @Override
public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { public ConfigurationPropertyState containsDescendantOf(
ConfigurationPropertyName name) {
return ConfigurationPropertyState.search(this, name::isAncestorOf); return ConfigurationPropertyState.search(this, name::isAncestorOf);
} }

@ -20,7 +20,6 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; 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(); public static final PropertyMapper INSTANCE = new SystemEnvironmentPropertyMapper();
private final ConfigurationPropertyNameBuilder nameBuilder = new ConfigurationPropertyNameBuilder(
this::createElement);
private SystemEnvironmentPropertyMapper() { private SystemEnvironmentPropertyMapper() {
} }
@ -57,7 +53,7 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper {
public List<PropertyMapping> map(PropertySource<?> propertySource, public List<PropertyMapping> map(PropertySource<?> propertySource,
String propertySourceName) { String propertySourceName) {
ConfigurationPropertyName name = convertName(propertySourceName); ConfigurationPropertyName name = convertName(propertySourceName);
if (name == null) { if (name == null || name.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
if (propertySourceName.endsWith("__")) { if (propertySourceName.endsWith("__")) {
@ -69,7 +65,8 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper {
private ConfigurationPropertyName convertName(String propertySourceName) { private ConfigurationPropertyName convertName(String propertySourceName) {
try { try {
return this.nameBuilder.from(propertySourceName, '_'); return ConfigurationPropertyName.adapt(propertySourceName, '_',
this::processElementValue);
} }
catch (Exception ex) { catch (Exception ex) {
return null; return null;
@ -85,8 +82,7 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper {
String[] elements = StringUtils String[] elements = StringUtils
.commaDelimitedListToStringArray(String.valueOf(value)); .commaDelimitedListToStringArray(String.valueOf(value));
for (int i = 0; i < elements.length; i++) { for (int i = 0; i < elements.length; i++) {
ConfigurationPropertyName name = ConfigurationPropertyName ConfigurationPropertyName name = rootName.append("[" + i + "]");
.of(rootName.toString() + "[" + i + "]");
mappings.add(new PropertyMapping(propertySourceName, name, mappings.add(new PropertyMapping(propertySourceName, name,
new ElementExtractor(i))); new ElementExtractor(i)));
} }
@ -106,33 +102,38 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper {
return result; return result;
} }
private String convertName(ConfigurationPropertyName configurationPropertyName) { private String convertName(ConfigurationPropertyName name) {
return configurationPropertyName.stream() return convertName(name, name.getNumberOfElements());
.map(name -> name.getValue(Form.UNIFORM).toUpperCase()) }
.collect(Collectors.joining("_"));
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) { private boolean isListShortcutPossible(ConfigurationPropertyName name) {
return (name.getElement().isIndexed() return (name.isLastElementIndexed() && isNumber(name.getLastElement(Form.UNIFORM))
&& isNumber(name.getElement().getValue(Form.UNIFORM)) && name.getNumberOfElements() >= 1);
&& name.getParent() != null);
} }
private List<PropertyMapping> mapListShortcut(PropertySource<?> propertySource, private List<PropertyMapping> mapListShortcut(PropertySource<?> propertySource,
ConfigurationPropertyName configurationPropertyName) { ConfigurationPropertyName name) {
String propertyName = convertName(configurationPropertyName.getParent()) + "__"; String result = convertName(name, name.getNumberOfElements() - 1) + "__";
if (propertySource.containsProperty(propertyName)) { if (propertySource.containsProperty(result)) {
int index = Integer.parseInt( int index = Integer.parseInt(name.getLastElement(Form.UNIFORM));
configurationPropertyName.getElement().getValue(Form.UNIFORM)); return Collections.singletonList(
return Collections.singletonList(new PropertyMapping(propertyName, new PropertyMapping(result, name, new ElementExtractor(index)));
configurationPropertyName, new ElementExtractor(index)));
} }
return Collections.emptyList(); return Collections.emptyList();
} }
private String createElement(String value) { private CharSequence processElementValue(CharSequence value) {
value = value.toLowerCase(); String result = value.toString().toLowerCase();
return (isNumber(value) ? "[" + value + "]" : value); return (isNumber(result) ? "[" + result + "]" : result);
} }
private static boolean isNumber(String string) { private static boolean isNumber(String string) {

@ -58,6 +58,7 @@ public class FileEncodingApplicationListenerTests {
Assume.assumeNotNull(System.getProperty("file.encoding")); Assume.assumeNotNull(System.getProperty("file.encoding"));
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
"spring.mandatory_file_encoding:" + System.getProperty("file.encoding")); "spring.mandatory_file_encoding:" + System.getProperty("file.encoding"));
ConfigurationPropertySources.attach(this.environment);
this.initializer.onApplicationEvent(this.event); this.initializer.onApplicationEvent(this.event);
} }

@ -129,7 +129,6 @@ public class MapBinderTests {
this.sources.add(source); this.sources.add(source);
Map<String, Map<String, Integer>> result = this.binder Map<String, Map<String, Integer>> result = this.binder
.bind("foo", Bindable.<Map<String, Map<String, Integer>>>of(type)).get(); .bind("foo", Bindable.<Map<String, Map<String, Integer>>>of(type)).get();
System.out.println(result);
assertThat(result).hasSize(2); assertThat(result).hasSize(2);
assertThat(result.get("bar")).containsEntry("baz", 1).containsEntry("bin", 2); assertThat(result.get("bar")).containsEntry("baz", 1).containsEntry("bin", 2);
assertThat(result.get("far")).containsEntry("baz", 3).containsEntry("bin", 4); assertThat(result.get("far")).containsEntry("baz", 3).containsEntry("bin", 4);

@ -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<Element> elements(String... elements) {
return Arrays.stream(elements).map(Element::new).collect(Collectors.toList());
}
private List<Element> getElements(String name) {
List<Element> elements = new ArrayList<>();
for (Element element : this.builder.from(name, '.')) {
elements.add(element);
}
return elements;
}
}

@ -18,7 +18,6 @@ package org.springframework.boot.context.properties.source;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -26,7 +25,6 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Element;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -48,7 +46,7 @@ public class ConfigurationPropertyNameTests {
public void ofNameShouldNotBeNull() throws Exception { public void ofNameShouldNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Name must not be null"); this.thrown.expectMessage("Name must not be null");
ConfigurationPropertyName.of((String) null); ConfigurationPropertyName.of(null);
} }
@Test @Test
@ -68,14 +66,14 @@ public class ConfigurationPropertyNameTests {
@Test @Test
public void ofNameShouldNotStartWithDot() throws Exception { public void ofNameShouldNotStartWithDot() throws Exception {
this.thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Name must not start with '.'"); this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of(".foo"); ConfigurationPropertyName.of(".foo");
} }
@Test @Test
public void ofNameShouldNotEndWithDot() throws Exception { public void ofNameShouldNotEndWithDot() throws Exception {
this.thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Name must not end with '.'"); this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of("foo."); ConfigurationPropertyName.of("foo.");
} }
@ -104,48 +102,63 @@ public class ConfigurationPropertyNameTests {
public void ofNameWhenSimple() throws Exception { public void ofNameWhenSimple() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("name"); ConfigurationPropertyName name = ConfigurationPropertyName.of("name");
assertThat(name.toString()).isEqualTo("name"); assertThat(name.toString()).isEqualTo("name");
assertThat((Object) name.getParent()).isNull(); assertThat(name.getNumberOfElements()).isEqualTo(1);
assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("name"); assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("name");
assertThat(name.isIndexed(0)).isFalse();
} }
@Test @Test
public void ofNameWhenRunOnAssociative() throws Exception { public void ofNameWhenRunOnAssociative() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[bar]"); ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[bar]");
assertThat(name.toString()).isEqualTo("foo[bar]"); assertThat(name.toString()).isEqualTo("foo[bar]");
assertThat(name.getParent().toString()).isEqualTo("foo"); assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo");
assertThat(name.getElement().toString()).isEqualTo("[bar]"); assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("bar");
assertThat(name.isIndexed(0)).isFalse();
assertThat(name.isIndexed(1)).isTrue();
} }
@Test @Test
public void ofNameWhenDotOnAssociative() throws Exception { public void ofNameWhenDotOnAssociative() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar"); ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar");
assertThat(name.toString()).isEqualTo("foo.bar"); assertThat(name.toString()).isEqualTo("foo.bar");
assertThat(name.getParent().toString()).isEqualTo("foo"); assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo");
assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("bar"); assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("bar");
assertThat(name.isIndexed(0)).isFalse();
assertThat(name.isIndexed(1)).isFalse();
} }
@Test @Test
public void ofNameWhenDotAndAssociative() throws Exception { public void ofNameWhenDotAndAssociative() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.[bar]"); ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.[bar]");
assertThat(name.toString()).isEqualTo("foo[bar]"); assertThat(name.toString()).isEqualTo("foo[bar]");
assertThat(name.getParent().toString()).isEqualTo("foo"); assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo");
assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("bar"); assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("bar");
assertThat(name.isIndexed(0)).isFalse();
assertThat(name.isIndexed(1)).isTrue();
} }
@Test @Test
public void ofNameWhenDoubleRunOnAndAssociative() throws Exception { public void ofNameWhenDoubleRunOnAndAssociative() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[bar]baz"); ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[bar]baz");
assertThat(name.toString()).isEqualTo("foo[bar].baz"); assertThat(name.toString()).isEqualTo("foo[bar].baz");
assertThat(name.getParent().toString()).isEqualTo("foo[bar]"); assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo");
assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("baz"); 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 @Test
public void ofNameWhenDoubleDotAndAssociative() throws Exception { public void ofNameWhenDoubleDotAndAssociative() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.[bar].baz"); ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.[bar].baz");
assertThat(name.toString()).isEqualTo("foo[bar].baz"); assertThat(name.toString()).isEqualTo("foo[bar].baz");
assertThat(name.getParent().toString()).isEqualTo("foo[bar]"); assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo");
assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("baz"); 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 @Test
@ -173,115 +186,207 @@ public class ConfigurationPropertyNameTests {
public void ofNameWithWhitespaceInAssociativeElement() throws Exception { public void ofNameWithWhitespaceInAssociativeElement() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[b a r]"); ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[b a r]");
assertThat(name.toString()).isEqualTo("foo[b a r]"); assertThat(name.toString()).isEqualTo("foo[b a r]");
assertThat(name.getParent().toString()).isEqualTo("foo"); assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo");
assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("b a r"); assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("b a r");
assertThat(name.isIndexed(0)).isFalse();
assertThat(name.isIndexed(1)).isTrue();
} }
@Test @Test
public void ofNameWithUppercaseInAssociativeElement() throws Exception { public void ofNameWithUppercaseInAssociativeElement() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[BAR]"); ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[BAR]");
assertThat(name.toString()).isEqualTo("foo[BAR]"); assertThat(name.toString()).isEqualTo("foo[BAR]");
assertThat(name.getParent().toString()).isEqualTo("foo"); assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo");
assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("BAR"); assertThat(name.getElement(1, Form.ORIGINAL)).isEqualTo("BAR");
assertThat(name.isIndexed(0)).isFalse();
assertThat(name.isIndexed(1)).isTrue();
} }
@Test @Test
public void equalsAndHashCode() throws Exception { public void ofWhenNameIsEmptyShouldReturnEmptyName() throws Exception {
ConfigurationPropertyName name1 = ConfigurationPropertyName.of("foo[bar]"); ConfigurationPropertyName name = ConfigurationPropertyName.of("");
ConfigurationPropertyName name2 = ConfigurationPropertyName.of("foo[bar]"); assertThat(name.toString()).isEqualTo("");
ConfigurationPropertyName name3 = ConfigurationPropertyName.of("foo.bar"); assertThat(name.append("foo").toString()).isEqualTo("foo");
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);
}
@Test
public void elementNameShouldNotIncludeAngleBrackets() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("[foo]");
assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("foo");
} }
@Test @Test
public void elementNameShouldNotIncludeDashes() throws Exception { public void adaptWhenNameIsNullShouldThrowException() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("f-o-o"); this.thrown.expect(IllegalArgumentException.class);
assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("foo"); this.thrown.expectMessage("Name must not be null");
ConfigurationPropertyName.adapt(null, '.');
}
@Test
public void adaptWhenElementValueProcessorIsNullShouldThrowException()
throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ElementValueProcessor must not be null");
ConfigurationPropertyName.adapt("foo", '.', null);
} }
@Test @Test
public void streamShouldReturnElements() throws Exception { public void adaptShouldCreateName() throws Exception {
assertThat(streamElements("foo.bar")).containsExactly("foo", "bar"); ConfigurationPropertyName expected = ConfigurationPropertyName.of("foo.bar.baz");
assertThat(streamElements("foo[0]")).containsExactly("foo", "[0]"); ConfigurationPropertyName name = ConfigurationPropertyName.adapt("foo.bar.baz",
assertThat(streamElements("foo.[0]")).containsExactly("foo", "[0]"); '.');
assertThat(streamElements("foo[baz]")).containsExactly("foo", "[baz]"); assertThat(name).isEqualTo(expected);
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<String> streamElements(String name) { @Test
return ConfigurationPropertyName.of(name).stream().map(Element::toString) public void adapShouldStripInvalidChars() throws Exception {
.iterator(); 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 @Test
public void elementIsIndexedWhenIndexedShouldReturnTrue() throws Exception { public void adaptShouldSupportIndexedElements() throws Exception {
assertThat(ConfigurationPropertyName.of("foo[0]").getElement().isIndexed()) 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(); .isTrue();
} }
@Test @Test
public void elementIsIndexedWhenNotIndexedShouldReturnFalse() throws Exception { public void isLastElementIndexedWhenNotIndexedShouldReturnFalse() throws Exception {
assertThat(ConfigurationPropertyName.of("foo.bar").getElement().isIndexed()) assertThat(ConfigurationPropertyName.of("foo.bar").isLastElementIndexed())
.isFalse();
assertThat(ConfigurationPropertyName.of("foo[0].bar").isLastElementIndexed())
.isFalse(); .isFalse();
} }
@Test @Test
public void isAncestorOfWhenSameShouldReturnFalse() throws Exception { public void getLastElementShouldGetLastElement() throws Exception {
ConfigurationPropertyName parent = ConfigurationPropertyName.of("foo"); ConfigurationPropertyName name = ConfigurationPropertyName.adapt("foo.bAr", '.');
assertThat(parent.isAncestorOf(parent)).isFalse(); assertThat(name.getLastElement(Form.ORIGINAL)).isEqualTo("bAr");
assertThat(name.getLastElement(Form.UNIFORM)).isEqualTo("bar");
} }
@Test @Test
public void isAncestorOfWhenParentShouldReturnFalse() throws Exception { public void getLastElementWhenEmptyShouldReturnEmptyString() throws Exception {
ConfigurationPropertyName parent = ConfigurationPropertyName.of("foo"); ConfigurationPropertyName name = ConfigurationPropertyName.EMPTY;
ConfigurationPropertyName child = ConfigurationPropertyName.of("foo.bar"); assertThat(name.getLastElement(Form.ORIGINAL)).isEqualTo("");
assertThat(parent.isAncestorOf(child)).isTrue(); assertThat(name.getLastElement(Form.UNIFORM)).isEqualTo("");
assertThat(child.isAncestorOf(parent)).isFalse();
} }
@Test @Test
public void isAncestorOfWhenGrandparentShouldReturnFalse() throws Exception { public void getElementShouldNotIncludeAngleBrackets() throws Exception {
ConfigurationPropertyName parent = ConfigurationPropertyName.of("foo"); ConfigurationPropertyName name = ConfigurationPropertyName.of("[foo]");
ConfigurationPropertyName grandchild = ConfigurationPropertyName assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("foo");
.of("foo.bar.baz"); assertThat(name.getElement(0, Form.UNIFORM)).isEqualTo("foo");
assertThat(parent.isAncestorOf(grandchild)).isTrue();
assertThat(grandchild.isAncestorOf(parent)).isFalse();
} }
@Test @Test
public void isAncestorOfWhenRootReturnTrue() throws Exception { public void getElementInUniformFormShouldNotIncludeDashes() throws Exception {
ConfigurationPropertyName parent = ConfigurationPropertyName.of(""); ConfigurationPropertyName name = ConfigurationPropertyName.of("f-o-o");
ConfigurationPropertyName grandchild = ConfigurationPropertyName assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("f-o-o");
.of("foo.bar.baz"); assertThat(name.getElement(0, Form.UNIFORM)).isEqualTo("foo");
assertThat(parent.isAncestorOf(grandchild)).isTrue(); }
assertThat(grandchild.isAncestorOf(parent)).isFalse();
@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<CharSequence> getElements(String name, Form form) {
ConfigurationPropertyName propertyName = ConfigurationPropertyName.of(name);
List<CharSequence> 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 @Test
@ -294,7 +399,7 @@ public class ConfigurationPropertyNameTests {
public void appendWhenIndexedShouldAppendWithBrackets() throws Exception { public void appendWhenIndexedShouldAppendWithBrackets() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo") ConfigurationPropertyName name = ConfigurationPropertyName.of("foo")
.append("[bar]"); .append("[bar]");
assertThat(name.getElement().isIndexed()).isTrue(); assertThat(name.isLastElementIndexed()).isTrue();
assertThat(name.toString()).isEqualTo("foo[bar]"); assertThat(name.toString()).isEqualTo("foo[bar]");
} }
@ -305,12 +410,103 @@ public class ConfigurationPropertyNameTests {
ConfigurationPropertyName.of("foo").append("1bar"); 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 @Test
public void appendWhenElementNameIsNullShouldReturnName() throws Exception { public void appendWhenElementNameIsNullShouldReturnName() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); ConfigurationPropertyName name = ConfigurationPropertyName.of("foo");
assertThat((Object) name.append((String) null)).isSameAs(name); 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 @Test
public void compareShouldSortNames() throws Exception { public void compareShouldSortNames() throws Exception {
List<ConfigurationPropertyName> names = new ArrayList<>(); List<ConfigurationPropertyName> names = new ArrayList<>();
@ -327,10 +523,41 @@ public class ConfigurationPropertyNameTests {
} }
@Test @Test
public void ofNameCanBeEmpty() throws Exception { public void toStringShouldBeLowerCaseDashed() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of(""); ConfigurationPropertyName name = ConfigurationPropertyName.adapt("fOO.b_-a-r",
assertThat(name.toString()).isEqualTo(""); '.');
assertThat(name.append("foo").toString()).isEqualTo("foo"); 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 @Test

Loading…
Cancel
Save