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;
}
/**
* 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 <T> The aggregate type

@ -42,6 +42,8 @@ import org.springframework.util.MultiValueMap;
*/
abstract class IndexedElementsBinder<T> extends AggregateBinder<T> {
private static final String INDEX_ZERO = "[0]";
IndexedElementsBinder(BindContext context) {
super(context);
}
@ -81,12 +83,13 @@ abstract class IndexedElementsBinder<T> extends AggregateBinder<T> {
MultiValueMap<String, ConfigurationProperty> 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<T> extends AggregateBinder<T> {
}
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);
}

@ -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<Map<Object, Object>> {
}
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<Map<Object, Object>> {
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<Map<Object, Object>> {
}
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();
}
}

@ -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<ObjectError> {
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());
}
/**

@ -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.
* <p>
* 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;
* <li>{@code server.hosts[0].name}</li>
* <li>{@code log[org.springboot].level}</li>
* </ul>
* <p>
*
* @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<Element>, Comparable<ConfigurationPropertyName> {
implements Comparable<ConfigurationPropertyName> {
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;
private ConfigurationPropertyName(CharSequence[] elements) {
this(elements, new CharSequence[elements.length]);
}
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,
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;
public boolean isLastElementIndexed() {
int size = getNumberOfElements();
return (size > 0 && isIndexed(this.elements[size - 1]));
}
@Override
public Iterator<Element> iterator() {
return stream().iterator();
/**
* 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
*/
boolean isIndexed(int elementIndex) {
return isIndexed(this.elements[elementIndex]);
}
/**
* Return a stream of the {@link Element Elements} that make up this name.
* @return a stream of {@link Element} items
* Return the last element in the name in the given form.
* @param form the form to return
* @return the last element
*/
public Stream<Element> stream() {
if (this.parent == null) {
return Stream.of(this.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));
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
* 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
* 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 Stream<Element> stream(ConfigurationPropertyName root) {
if (this.parent == null || this.parent.equals(root)) {
return Stream.of(this.element);
public ConfigurationPropertyName append(String elementValue) {
if (elementValue == null) {
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() {
if (this.toString == null) {
this.toString = buildToString();
/**
* 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;
}
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();
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 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) or
* 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<Element> elements = iterator();
Iterator<Element> 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;
if (getNumberOfElements() != other.getNumberOfElements()) {
return false;
}
for (int i = 0; i < this.elements.length; i++) {
if (!elementEquals(this.elements[i], other.elements[i])) {
return false;
}
}
return true;
}
/**
* 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);
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;
}
/**
* 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);
}
return this;
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<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> {
private static final Pattern VALUE_PATTERN = Pattern.compile("[\\w\\-]+");
private final boolean indexed;
private final String[] value;
static ConfigurationPropertyName adapt(CharSequence name, char separator) {
return adapt(name, separator, Function.identity());
}
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.
* <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;
}
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<CharSequence> elements = new ArrayList<CharSequence>(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 getValue(Form.UNIFORM).compareTo(other.getValue(Form.UNIFORM));
}
@Override
public int hashCode() {
return getValue(Form.UNIFORM).hashCode();
}
});
return new ConfigurationPropertyName(
elements.toArray(new CharSequence[elements.size()]));
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
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;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
else if (!indexed && ch == '[') {
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);
}
/**
* 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()];
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);
}
}
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:
* <ul>
* <li>"{@code foo-bar}" = "{@code foo-bar}"</li>
* <li>"{@code fooBar}" = "{@code fooBar}"</li>
@ -414,33 +557,7 @@ public final class ConfigurationPropertyName
* <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li>
* </ul>
*/
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 == '-');
}
},
ORIGINAL,
/**
* 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>
* </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 {
/**
* 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));
}
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);
}
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 == '-';
}
}

@ -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 final ConfigurationPropertyNameBuilder nameBuilder = new ConfigurationPropertyNameBuilder();
private DefaultPropertyMapper() {
}
@ -76,14 +74,17 @@ final class DefaultPropertyMapper implements PropertyMapper {
private List<PropertyMapping> 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<T> {

@ -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

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

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

@ -27,10 +27,10 @@ import org.springframework.core.env.PropertySource;
* <P>
* 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.
*

@ -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;
}

@ -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);
}

@ -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<PropertyMapping> 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<PropertyMapping> 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) {

@ -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);
}

@ -129,7 +129,6 @@ public class MapBinderTests {
this.sources.add(source);
Map<String, Map<String, Integer>> result = this.binder
.bind("foo", Bindable.<Map<String, Map<String, Integer>>>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);

@ -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.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);
}
@Test
public void elementNameShouldNotIncludeAngleBrackets() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("[foo]");
assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("foo");
public void ofWhenNameIsEmptyShouldReturnEmptyName() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("");
assertThat(name.toString()).isEqualTo("");
assertThat(name.append("foo").toString()).isEqualTo("foo");
}
@Test
public void elementNameShouldNotIncludeDashes() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("f-o-o");
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 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");
public void adaptShouldCreateName() throws Exception {
ConfigurationPropertyName expected = ConfigurationPropertyName.of("foo.bar.baz");
ConfigurationPropertyName name = ConfigurationPropertyName.adapt("foo.bar.baz",
'.');
assertThat(name).isEqualTo(expected);
}
private Iterator<String> streamElements(String name) {
return ConfigurationPropertyName.of(name).stream().map(Element::toString)
.iterator();
@Test
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 elementIsIndexedWhenIndexedShouldReturnTrue() throws Exception {
assertThat(ConfigurationPropertyName.of("foo[0]").getElement().isIndexed())
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<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
@ -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<ConfigurationPropertyName> 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

Loading…
Cancel
Save