/* * 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.Collection; import java.util.List; import java.util.Map; import java.util.function.Function; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** * 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 * restricted and are considered case-sensitive. * <p> * Here are some typical examples: * <ul> * <li>{@code spring.main.banner-mode}</li> * <li>{@code server.hosts[0].name}</li> * <li>{@code log[org.springboot].level}</li> * </ul> * * @author Phillip Webb * @author Madhura Bhave * @since 2.0.0 * @see #of(CharSequence) * @see ConfigurationPropertySource */ public final class ConfigurationPropertyName implements Comparable<ConfigurationPropertyName> { private static final String EMPTY_STRING = ""; /** * An empty {@link ConfigurationPropertyName}. */ public static final ConfigurationPropertyName EMPTY = new ConfigurationPropertyName( new String[0]); private final CharSequence[] elements; private final CharSequence[] uniformElements; private int[] elementHashCodes; private String string; private ConfigurationPropertyName(CharSequence[] elements) { this(elements, new CharSequence[elements.length]); } private ConfigurationPropertyName(CharSequence[] elements, CharSequence[] uniformElements) { this.elements = elements; this.uniformElements = uniformElements; } /** * Returns {@code true} if this {@link ConfigurationPropertyName} is empty. * @return {@code true} if the name is empty */ public boolean isEmpty() { return this.elements.length == 0; } /** * Return if the last element in the name is indexed. * @return {@code true} if the last element is indexed */ public boolean isLastElementIndexed() { int size = getNumberOfElements(); return (size > 0 && isIndexed(this.elements[size - 1])); } /** * 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 the last element in the name in the given form. * @param form the form to return * @return the last element */ public String getLastElement(Form form) { int size = getNumberOfElements(); return (size == 0 ? EMPTY_STRING : getElement(size - 1, form)); } /** * Return an element in the name in the given form. * @param elementIndex the element index * @param form the form to return * @return the last element */ public String getElement(int elementIndex, Form form) { if (form == Form.ORIGINAL) { CharSequence result = this.elements[elementIndex]; if (isIndexed(result)) { result = result.subSequence(1, result.length() - 1); } return result.toString(); } 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; } /** * Create a new {@link ConfigurationPropertyName} by appending the given element * value. * @param elementValue the single element value to append * @return a new {@link ConfigurationPropertyName} */ public ConfigurationPropertyName append(String elementValue) { if (elementValue == null) { return this; } process(elementValue, '.', (value, start, end, indexed) -> Assert.isTrue( start == 0, () -> "Element value '" + elementValue + "' must be a single item")); Assert.isTrue( isIndexed(elementValue) || ElementValidator.isValidElement(elementValue), () -> "Element value '" + elementValue + "' is not valid"); int length = this.elements.length; CharSequence[] elements = new CharSequence[length + 1]; System.arraycopy(this.elements, 0, elements, 0, length); elements[length] = elementValue; CharSequence[] uniformElements = new CharSequence[length + 1]; System.arraycopy(this.uniformElements, 0, uniformElements, 0, length); return new ConfigurationPropertyName(elements, uniformElements); } /** * Return a new {@link ConfigurationPropertyName} by chopping this name to the given * {@code size}. For example, {@code chop(1)} on the name {@code foo.bar} will return * {@code foo}. * @param size the size to chop * @return the chopped name */ public ConfigurationPropertyName chop(int size) { if (size >= getNumberOfElements()) { return this; } CharSequence[] elements = new CharSequence[size]; System.arraycopy(this.elements, 0, elements, 0, size); CharSequence[] uniformElements = new CharSequence[size]; System.arraycopy(this.uniformElements, 0, uniformElements, 0, size); return new ConfigurationPropertyName(elements, uniformElements); } /** * Returns {@code true} if this element is an immediate parent of the specified name. * @param name the name to check * @return {@code true} if this name is an ancestor */ public boolean isParentOf(ConfigurationPropertyName name) { Assert.notNull(name, "Name must not be null"); if (this.getNumberOfElements() != name.getNumberOfElements() - 1) { return false; } return isAncestorOf(name); } /** * Returns {@code true} if this element is an ancestor (immediate or nested parent) of * the specified name. * @param name the name to check * @return {@code true} if this name is an ancestor */ public boolean isAncestorOf(ConfigurationPropertyName name) { Assert.notNull(name, "Name must not be null"); if (this.getNumberOfElements() >= name.getNumberOfElements()) { return false; } for (int i = 0; i < this.elements.length; i++) { if (!elementEquals(this.elements[i], name.elements[i])) { return false; } } return true; } @Override public int compareTo(ConfigurationPropertyName other) { 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; } } return 0; } private int compare(String e1, boolean indexed1, String e2, boolean indexed2) { if (e1 == null) { return -1; } if (e2 == null) { return 1; } 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() { 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 (obj == this) { return true; } if (obj == null || !obj.getClass().equals(getClass())) { return false; } ConfigurationPropertyName other = (ConfigurationPropertyName) obj; 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; } private boolean elementEquals(CharSequence e1, CharSequence e2) { int l1 = e1.length(); int l2 = e2.length(); boolean indexed1 = isIndexed(e1); int offset1 = (indexed1 ? 1 : 0); boolean indexed2 = isIndexed(e2); int offset2 = (indexed2 ? 1 : 0); int i1 = offset1; int i2 = offset2; while (i1 < l1 - offset1) { if (i2 >= l2 - offset2) { return false; } char ch1 = (indexed1 ? e1.charAt(i1) : Character.toLowerCase(e1.charAt(i1))); char ch2 = (indexed2 ? e2.charAt(i2) : Character.toLowerCase(e2.charAt(i2))); if (ch1 == '-' || ch1 == '_') { i1++; } else if (ch2 == '-' || ch2 == '_') { i2++; } else if (ch1 != ch2) { return false; } else { i1++; i2++; } } while (i2 < l2 - offset2) { char ch = e2.charAt(i2++); if (ch != '-' && ch != '_') { return false; } } return true; } private static boolean isIndexed(CharSequence element) { int length = element.length(); return length > 2 && element.charAt(0) == '[' && element.charAt(length - 1) == ']'; } /** * Returns if the given name is valid. If this method returns {@code true} then the * name may be used with {@link #of(CharSequence)} without throwing an exception. * @param name the name to test * @return {@code true} if the name is valid */ public static boolean isValid(CharSequence name) { if (name == null) { return false; } if (name.equals(EMPTY_STRING)) { return true; } if (name.charAt(0) == '.' || name.charAt(name.length() - 1) == '.') { return false; } ElementValidator validator = new ElementValidator(); process(name, '.', validator); return validator.isValid(); } /** * Return a {@link ConfigurationPropertyName} for the specified string. * @param name the source name * @return a {@link ConfigurationPropertyName} instance * @throws IllegalArgumentException if the name is not valid */ public static ConfigurationPropertyName of(CharSequence name) { Assert.notNull(name, "Name must not be null"); 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; } 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()])); } /** * 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} */ static ConfigurationPropertyName adapt(CharSequence name, char separator) { return adapt(name, separator, Function.identity()); } /** * 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; } 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); } if (elementValue.length() > 0) { elements.add(elementValue); } }); return new ConfigurationPropertyName( elements.toArray(new CharSequence[elements.size()])); } private static void process(CharSequence name, char separator, ElementProcessor processor) { int start = 0; boolean indexed = false; int length = name.length(); for (int i = 0; i < length; i++) { char ch = name.charAt(i); if (indexed && ch == ']') { processElement(processor, name, start, i + 1, indexed); start = i + 1; indexed = false; } 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; } } processElement(processor, name, start, length, false); } 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); } } 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 name; } /** * The various forms that a non-indexed element value can take. */ public enum Form { /** * 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> * <li>"{@code foo_bar}" = "{@code foo_bar}"</li> * <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li> * </ul> */ ORIGINAL, /** * The uniform configuration form (used for equals/hashCode; lower-case with only * alphanumeric characters). * <ul> * <li>"{@code foo-bar}" = "{@code foobar}"</li> * <li>"{@code fooBar}" = "{@code foobar}"</li> * <li>"{@code foo_bar}" = "{@code foobar}"</li> * <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li> * </ul> */ UNIFORM } /** * Internal functional interface used when processing names. */ @FunctionalInterface private interface ElementProcessor { void process(CharSequence elementValue, int start, int end, boolean indexed); } /** * Internal filter used to strip out characters. */ private interface CharFilter { boolean isExcluded(char ch, int index); } /** * Internal processor used to change characters. */ private interface CharProcessor { CharProcessor NONE = (c, i) -> c; CharProcessor LOWERCASE = (c, i) -> Character.toLowerCase(c); char process(char c, int index); } /** * {@link ElementProcessor} that checks if a name is valid. */ private static class ElementValidator implements ElementProcessor { private boolean valid = true; @Override public void process(CharSequence elementValue, int start, int end, boolean indexed) { if (this.valid && !indexed) { this.valid = isValidElement(elementValue); } } 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; } 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 isAlpha || isNumeric || ch == '-'; } } }