diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/package-info.java index 6b1693d24c..9b32cf99cd 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/package-info.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -15,8 +15,7 @@ */ /** - * Support for external configuration binding via the {@code @ConfigurationProperties} - * annotation. + * Support for external configuration properties. * * @see org.springframework.boot.context.properties.ConfigurationProperties * @see org.springframework.boot.context.properties.EnableConfigurationProperties diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySource.java new file mode 100644 index 0000000000..acfb907db9 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySource.java @@ -0,0 +1,73 @@ +/* + * 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.List; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * A {@link ConfigurationPropertySource} supporting name aliases. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class AliasedConfigurationPropertySource implements ConfigurationPropertySource { + + private final ConfigurationPropertySource source; + + private final ConfigurationPropertyNameAliases aliases; + + AliasedConfigurationPropertySource(ConfigurationPropertySource source, + ConfigurationPropertyNameAliases aliases) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(aliases, "Aliases must not be null"); + this.source = source; + this.aliases = aliases; + } + + @Override + public Stream stream() { + return StreamSupport.stream(this.source.spliterator(), false) + .flatMap(this::addAliases); + } + + private Stream addAliases(ConfigurationPropertyName name) { + Stream names = Stream.of(name); + List aliases = this.aliases.getAliases(name); + if (CollectionUtils.isEmpty(aliases)) { + return names; + } + return Stream.concat(names, aliases.stream()); + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + Assert.notNull(name, "Name must not be null"); + ConfigurationProperty result = this.source.getConfigurationProperty(name); + if (result == null) { + ConfigurationPropertyName aliasedName = this.aliases.getNameForAlias(name); + result = this.source.getConfigurationProperty(aliasedName); + } + return result; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationProperty.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationProperty.java new file mode 100644 index 0000000000..30ded56ab3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationProperty.java @@ -0,0 +1,115 @@ +/* + * 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 org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A single configuration property obtained from a {@link ConfigurationPropertySource} + * consisting of a {@link #getName() name}, {@link #getValue() value} and optional + * {@link #getOrigin() origin}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public final class ConfigurationProperty + implements OriginProvider, Comparable { + + private final ConfigurationPropertyName name; + + private final Object value; + + private final Origin origin; + + public ConfigurationProperty(ConfigurationPropertyName name, Object value, + Origin origin) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(value, "Value must not be null"); + this.name = name; + this.value = value; + this.origin = origin; + } + + public ConfigurationPropertyName getName() { + return this.name; + } + + public Object getValue() { + return this.value; + } + + @Override + public Origin getOrigin() { + return this.origin; + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(this.name); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.value); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ConfigurationProperty other = (ConfigurationProperty) obj; + boolean result = true; + result = result && ObjectUtils.nullSafeEquals(this.name, other.name); + result = result && ObjectUtils.nullSafeEquals(this.value, other.value); + return result; + } + + @Override + public String toString() { + return new ToStringCreator(this).append("name", this.name) + .append("value", this.value).append("origin", this.origin).toString(); + } + + @Override + public int compareTo(ConfigurationProperty other) { + return this.name.compareTo(other.name); + } + + static ConfigurationProperty of(ConfigurationPropertyName name, + OriginTrackedValue value) { + if (value == null) { + return null; + } + return new ConfigurationProperty(name, value.getValue(), value.getOrigin()); + } + + static ConfigurationProperty of(ConfigurationPropertyName name, Object value, + Origin origin) { + if (value == null) { + return null; + } + return new ConfigurationProperty(name, value, origin); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java new file mode 100644 index 0000000000..1680b6a616 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java @@ -0,0 +1,439 @@ +/* + * 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.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Element; +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}") & "{@code -}", they must be lower-case and + * must start with a letter. The "{@code -}" is used purely for formatting, i.e. + * "{@code foo-bar}" and "{@code foobar}" are considered equivalent. + *

+ * The "{@code [}" and "{@code ]}" characters may be used to indicate an associative + * index(i.e. a {@link Map} key or a {@link Collection} index. Indexes names are not + * restricted and are considered case-sensitive. + *

+ * Here are some typical examples: + *

    + *
  • {@code spring.main.banner-mode}
  • + *
  • {@code server.hosts[0].name}
  • + *
  • {@code log[org.springboot].level}
  • + *
+ *

+ * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see #of(String) + * @see ConfigurationPropertyNameBuilder + * @see ConfigurationPropertySource + */ +public final class ConfigurationPropertyName + implements Iterable, Comparable { + + /** + * An empty {@link ConfigurationPropertyName}. + */ + public static final ConfigurationPropertyName EMPTY = new ConfigurationPropertyName( + null, new Element()); + + private static final ConfigurationPropertyNameBuilder BUILDER = new ConfigurationPropertyNameBuilder( + Pattern.compile("[a-z]([a-z0-9\\-])*")); + + private final ConfigurationPropertyName parent; + + private final Element element; + + private String toString; + + ConfigurationPropertyName(ConfigurationPropertyName parent, Element element) { + Assert.notNull(element, "Element must not be null"); + this.parent = parent; + this.element = element; + } + + /** + * Return the parent of this configuration property. + * @return the parent or {code null} + */ + public ConfigurationPropertyName getParent() { + return this.parent; + } + + /** + * Return the element part of this configuration property name. + * @return the element (never {@code null}) + */ + public Element getElement() { + return this.element; + } + + @Override + public Iterator iterator() { + return stream().iterator(); + } + + /** + * Return a stream of the {@link Element Elements} that make up this name. + * @return a stream of {@link Element} items + */ + public Stream stream() { + if (this.parent == null) { + return Stream.of(this.element); + } + return Stream.concat(this.parent.stream(), Stream.of(this.element)); + } + + /** + * Return a stream of the {@link Element Elements} that make up this name starting + * from the given root. + * @param root the root of the name or {@code null} to stream all elements + * @return a stream of {@link Element} items + */ + public Stream stream(ConfigurationPropertyName root) { + if (this.parent == null || this.parent.equals(root)) { + return Stream.of(this.element); + } + return Stream.concat(this.parent.stream(root), Stream.of(this.element)); + } + + @Override + public String toString() { + if (this.toString == null) { + this.toString = buildToString(); + } + return this.toString; + } + + private String buildToString() { + StringBuilder result = new StringBuilder(); + result.append(this.parent != null ? this.parent.toString() : ""); + result.append(result.length() > 0 && !this.element.isIndexed() ? "." : ""); + result.append(this.element); + return result.toString(); + } + + /** + * Returns {@code true} if this element is an ancestor (immediate or nested parent) or + * the specified name. + * @param name the name to check + * @return {@code true} if this name is an ancessor + */ + public boolean isAncestorOf(ConfigurationPropertyName name) { + ConfigurationPropertyName candidate = (name == null ? null : name.getParent()); + while (candidate != null) { + if (candidate.equals(this)) { + return true; + } + candidate = candidate.getParent(); + } + return false; + } + + @Override + public int compareTo(ConfigurationPropertyName other) { + Iterator elements = iterator(); + Iterator otherElements = other.iterator(); + while (elements.hasNext() || otherElements.hasNext()) { + int result = compare(elements.hasNext() ? elements.next() : null, + otherElements.hasNext() ? otherElements.next() : null); + if (result != 0) { + return result; + } + } + return 0; + } + + private int compare(Element element, Element other) { + if (element == null) { + return -1; + } + if (other == null) { + return 1; + } + return element.compareTo(other); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + ObjectUtils.nullSafeHashCode(this.parent); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.element); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ConfigurationPropertyName other = (ConfigurationPropertyName) obj; + boolean result = true; + result = result && ObjectUtils.nullSafeEquals(this.parent, other.parent); + result = result && ObjectUtils.nullSafeEquals(this.element, other.element); + return result; + } + + /** + * Create a new {@link ConfigurationPropertyName} by appending the given index. + * @param index the index to append + * @return a new {@link ConfigurationPropertyName} + */ + public ConfigurationPropertyName appendIndex(int index) { + return append("[" + index + "]"); + } + + /** + * Create a new {@link ConfigurationPropertyName} by appending the given element. + * @param element the element to append + * @return a new {@link ConfigurationPropertyName} + */ + public ConfigurationPropertyName append(String element) { + if (StringUtils.hasLength(element)) { + return BUILDER.from(this).append(element).build(); + } + return this; + } + + /** + * 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(String name) + throws IllegalArgumentException { + Assert.notNull(name, "Name must not be null"); + Assert.isTrue(!name.toString().startsWith("."), "Name must not start with '.'"); + Assert.isTrue(!name.toString().endsWith("."), "Name must not end with '.'"); + if (StringUtils.isEmpty(name)) { + return EMPTY; + } + return BUILDER.from(name, '.').build(); + } + + /** + * An individual element of the {@link ConfigurationPropertyName}. + */ + public static final class Element implements Comparable { + + private static final Pattern VALUE_PATTERN = Pattern.compile("[\\w\\-]+"); + + private final boolean indexed; + + private final String[] value; + + private Element() { + this.indexed = false; + this.value = Form.expand("", false); + } + + Element(String value) { + Assert.notNull(value, "Value must not be null"); + this.indexed = (value.startsWith("[") && value.endsWith("]")); + value = (this.indexed ? value.substring(1, value.length() - 1) : value); + if (!this.indexed) { + validate(value); + } + 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 (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(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return ObjectUtils.nullSafeEquals(getValue(Form.UNIFORM), + ((Element) obj).getValue(Form.UNIFORM)); + } + + @Override + public String toString() { + String string = getValue(Form.CONFIGURATION).toString(); + 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; + } + + /** + * 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()]; + } + + } + + /** + * The various forms that a non-indexed {@link Element} {@code value} can take. + */ + public enum Form { + + /** + * The original form as specified when the name was created. For example: + *

    + *
  • "{@code foo-bar}" = "{@code foo-bar}"
  • + *
  • "{@code fooBar}" = "{@code fooBar}"
  • + *
  • "{@code foo_bar}" = "{@code foo_bar}"
  • + *
  • "{@code [Foo.bar]}" = "{@code Foo.bar}"
  • + *
+ */ + ORIGINAL { + + @Override + protected String convert(String value) { + return value; + } + + }, + + /** + * The canonical configuration form (lower-case with only alphanumeric and + * "{@code -}" characters). + *
    + *
  • "{@code foo-bar}" = "{@code foo-bar}"
  • + *
  • "{@code fooBar}" = "{@code foobar}"
  • + *
  • "{@code foo_bar}" = "{@code foobar}"
  • + *
  • "{@code [Foo.bar]}" = "{@code Foo.bar}"
  • + *
+ */ + CONFIGURATION { + + @Override + protected boolean isIncluded(char ch) { + return Character.isAlphabetic(ch) || Character.isDigit(ch) || (ch == '-'); + } + + }, + + /** + * The uniform configuration form (used for equals/hashcode; lower-case with only + * alphanumeric characters). + *
    + *
  • "{@code foo-bar}" = "{@code foobar}"
  • + *
  • "{@code fooBar}" = "{@code foobar}"
  • + *
  • "{@code foo_bar}" = "{@code foobar}"
  • + *
  • "{@code [Foo.bar]}" = "{@code Foo.bar}"
  • + *
+ */ + UNIFORM { + + @Override + protected boolean isIncluded(char ch) { + return Character.isAlphabetic(ch) || Character.isDigit(ch); + } + + }; + + /** + * Called to convert an original value into the instance form. + * @param value the value to convert + * @return the converted value + */ + protected String convert(String value) { + StringBuilder result = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + if (isIncluded(ch)) { + result.append(Character.toLowerCase(ch)); + } + } + return result.toString(); + } + + /** + * Called to determine of the specified character is valid for the form. + * @param ch the character to test + * @return if the character is value + */ + protected boolean isIncluded(char ch) { + 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)); + } + return result; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliases.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliases.java new file mode 100644 index 0000000000..022c124cdb --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliases.java @@ -0,0 +1,77 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Maintains a mapping of {@link ConfigurationPropertyName} alaises. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see ConfigurationPropertySource#withAliases(ConfigurationPropertyNameAliases) + */ +public final class ConfigurationPropertyNameAliases { + + private final MultiValueMap aliases = new LinkedMultiValueMap<>(); + + public ConfigurationPropertyNameAliases() { + } + + public ConfigurationPropertyNameAliases(String name, String... aliases) { + addAlaises(name, aliases); + } + + public ConfigurationPropertyNameAliases(ConfigurationPropertyName name, + ConfigurationPropertyName... aliases) { + addAlaises(name, aliases); + } + + public void addAlaises(String name, String... aliases) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(aliases, "Aliases must not be null"); + addAlaises(ConfigurationPropertyName.of(name), + Arrays.stream(aliases).map(ConfigurationPropertyName::of) + .toArray(ConfigurationPropertyName[]::new)); + } + + public void addAlaises(ConfigurationPropertyName name, + ConfigurationPropertyName... aliases) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(aliases, "Aliases must not be null"); + this.aliases.addAll(name, Arrays.asList(aliases)); + } + + public List getAliases(ConfigurationPropertyName name) { + return this.aliases.getOrDefault(name, Collections.emptyList()); + } + + public ConfigurationPropertyName getNameForAlias(ConfigurationPropertyName alias) { + return this.aliases.entrySet().stream() + .filter((e) -> e.getValue().contains(alias)).map(Map.Entry::getKey) + .findFirst().orElse(null); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilder.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilder.java new file mode 100644 index 0000000000..2738fc840d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilder.java @@ -0,0 +1,249 @@ +/* + * 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.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +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.CollectionUtils; + +/** + * 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 + * @since 2.0.0 + * @see ConfigurationPropertyName + */ +public class ConfigurationPropertyNameBuilder { + + private final ElementValueProcessor processor; + + private final List elements; + + /** + * Create a new {@link ConfigurationPropertyNameBuilder} instance. + */ + public ConfigurationPropertyNameBuilder() { + this(ElementValueProcessor.empty()); + } + + /** + * Create a new {@link ConfigurationPropertyNameBuilder} instance that enforces a + * {@link Pattern} on all element values. + * @param elementValuePattern the element value pattern to enforce + */ + public ConfigurationPropertyNameBuilder(Pattern elementValuePattern) { + this(ElementValueProcessor.empty().withPatternCheck(elementValuePattern)); + } + + /** + * Create a new {@link ConfigurationPropertyNameBuilder} with the specified + * {@link ElementValueProcessor}. + * @param processor the element value processor. + */ + public ConfigurationPropertyNameBuilder(ElementValueProcessor processor) { + Assert.notNull(processor, "Processor must not be null"); + this.processor = processor; + this.elements = Collections.emptyList(); + } + + /** + * Internal constructor used to create new builders. + * @param processor the element value processor. + * @param elements the elements built so far + */ + private ConfigurationPropertyNameBuilder(ElementValueProcessor processor, + List elements) { + this.processor = processor; + this.elements = elements; + } + + /** + * Start building 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 ConfigurationPropertyNameBuilder from(String name, char separator) { + Assert.notNull(name, "Name must not be null"); + List elements = new ArrayList<>(); + StringBuilder value = new StringBuilder(name.length()); + boolean indexed = false; + for (int i = 0; i < name.length(); i++) { + char ch = name.charAt(i); + if (!indexed) { + if (ch == '[') { + addElement(elements, value); + value.append(ch); + indexed = true; + } + else if (ch == separator) { + addElement(elements, value); + } + else { + value.append(ch); + } + } + else { + value.append(ch); + if (ch == ']') { + addElement(elements, value); + indexed = false; + } + } + } + addElement(elements, value); + return from(elements.stream().filter(Objects::nonNull) + .filter((e) -> !e.getValue(Form.UNIFORM).isEmpty()).iterator()); + } + + private void addElement(List elements, StringBuilder value) { + if (value.length() > 0) { + elements.add(buildElement(value.toString())); + value.setLength(0); + } + } + + /** + * Return a new {@link ConfigurationPropertyNameBuilder} starting with the specified + * elements. + * @param elements the elements that the new builder should contain + * @return a new initialized builder instance + */ + public ConfigurationPropertyNameBuilder from(Iterable elements) { + return from(elements.iterator()); + } + + /** + * Return a new {@link ConfigurationPropertyNameBuilder} starting with the specified + * elements. + * @param elements the elements that the new builder should contain + * @return a new initialized builder instance + */ + public ConfigurationPropertyNameBuilder from(Iterator elements) { + Assert.state(CollectionUtils.isEmpty(this.elements), + "Existing elements must not be present"); + return new ConfigurationPropertyNameBuilder(this.processor, toList(elements)); + } + + private List toList(Iterator iterator) { + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + if (isRoot(list)) { + return Collections.emptyList(); + } + return list; + } + + private boolean isRoot(List list) { + return (list.size() == 1 && list.get(0).getValue(Form.ORIGINAL).isEmpty()); + } + + /** + * Return a new builder containing the elements built so far appended with the + * specified element value. + * @param elementValue the element value to append + * @return a new builder instance + */ + public ConfigurationPropertyNameBuilder append(String elementValue) { + Assert.notNull(elementValue, "ElementValue must not be null"); + List elements = new ArrayList<>(this.elements); + elements.add(buildElement(elementValue)); + return new ConfigurationPropertyNameBuilder(this.processor, elements); + } + + private Element buildElement(String value) { + return new Element(this.processor.apply(value)); + } + + /** + * Build a new {@link ConfigurationPropertyName} from the elements contained in this + * builder. + * @return a new {@link ConfigurationPropertyName}. + */ + public ConfigurationPropertyName build() { + ConfigurationPropertyName name = null; + for (Element element : this.elements) { + name = new ConfigurationPropertyName(name, element); + } + Assert.state(name != null, "At least one element must be defined"); + return name; + } + + /** + * An processor that will be applied to element values. Can be used to manipulate or + * restrict the values that are used. + */ + @FunctionalInterface + 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; + + /** + * Extend this processor with a {@link Pattern} regular expression check. + * @param pattern the patter to check + * @return an element processor that additionally checks against the pattern + */ + default ElementValueProcessor withPatternCheck(Pattern pattern) { + Assert.notNull(pattern, "Pattern must not be null"); + return (value) -> { + value = apply(value); + Element element = new Element(value); + Assert.isTrue(element.isIndexed() || pattern.matcher(value).matches(), + "Element value '" + value + "' is not valid (" + pattern + + " does not match)"); + return value; + }; + } + + /** + * Return an empty {@link ElementValueProcessor} that simply returns the original + * value unchanged. + * @return an empty {@link ElementValueProcessor}. + */ + static ElementValueProcessor empty() { + return (value) -> value; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySource.java new file mode 100644 index 0000000000..4f75405787 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySource.java @@ -0,0 +1,87 @@ +/* + * 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.Iterator; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.core.env.PropertySource; + +/** + * A source of {@link ConfigurationProperty ConfigurationProperties}, usually backed by a + * Spring {@link PropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see ConfigurationPropertyName + * @see OriginTrackedValue + * @see #getConfigurationProperty(ConfigurationPropertyName) + */ +public interface ConfigurationPropertySource extends Iterable { + + /** + * Return a single {@link ConfigurationProperty} from the source or {@code null} if no + * property can be found. + * @param name the name of the property (must not be {@code null}) + * @return the associated object or {@code null}. + */ + ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name); + + /** + * Return an iterator for the {@link ConfigurationPropertyName names} managed by this + * source. If it is not possible to determine the names an empty iterator may be + * returned. + * @return an iterator (never {@code null}) + */ + @Override + default Iterator iterator() { + return stream().iterator(); + } + + /** + * Returns a sequential {@code Stream} for the {@link ConfigurationPropertyName names} + * managed by this source. If it is not possible to determine the names an + * {@link Stream#empty() empty stream} may be returned. + * @return a stream of names (never {@code null}) + */ + Stream stream(); + + /** + * Return a filtered variant of this source, containing only names that match the + * given {@link Predicate}. + * @param filter the filter to apply + * @return a filtered {@link ConfigurationPropertySource} instance + */ + default ConfigurationPropertySource filter( + Predicate filter) { + return new FilteredConfigurationPropertiesSource(this, filter); + } + + /** + * Return a variant of this source that supports name aliases. + * @param aliases a function that returns a stream of aliases for any given name + * @return a {@link ConfigurationPropertySource} instance supporting name alaises + */ + default ConfigurationPropertySource withAliases( + ConfigurationPropertyNameAliases aliases) { + return new AliasedConfigurationPropertySource(this, aliases); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java new file mode 100644 index 0000000000..be7fae3f46 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java @@ -0,0 +1,162 @@ +/* + * 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.Iterator; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySources; +import org.springframework.core.env.PropertySourcesPropertyResolver; +import org.springframework.core.env.SystemEnvironmentPropertySource; +import org.springframework.util.Assert; + +/** + * A managed set of {@link ConfigurationPropertySource} instances, usually adapted from + * Spring's {@link PropertySources}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + * @see #attach(MutablePropertySources) + * @see #get(PropertySources) + */ +public class ConfigurationPropertySources + implements Iterable { + + /** + * The name of the {@link PropertySource} {@link #adapt adapter}. + */ + public static final String PROPERTY_SOURCE_NAME = "configurationPropertes"; + + private final PropertySources propertySources; + + private final Map, ConfigurationPropertySource> adapters = new WeakHashMap<>(); + + /** + * Create a new {@link ConfigurationPropertySources} instance. + * @param propertySources the property sources to expose + */ + ConfigurationPropertySources(PropertySources propertySources) { + Assert.notNull(propertySources, "PropertySources must not be null"); + this.propertySources = propertySources; + } + + @Override + public Iterator iterator() { + return streamPropertySources(this.propertySources) + .filter(s -> !(s instanceof ConfigurationPropertySourcesPropertySource)) + .map(this::adapt).collect(Collectors.toList()).iterator(); + } + + private Stream> streamPropertySources(PropertySources sources) { + return StreamSupport.stream(sources.spliterator(), false).flatMap(this::flatten); + } + + private Stream> flatten(PropertySource source) { + if (source.getSource() instanceof ConfigurableEnvironment) { + return streamPropertySources( + ((ConfigurableEnvironment) source.getSource()).getPropertySources()); + } + return Stream.of(source); + } + + private ConfigurationPropertySource adapt(PropertySource source) { + return this.adapters.computeIfAbsent(source, (k) -> { + return new PropertySourceConfigurationPropertySource(source, + getPropertyMapper(source)); + }); + } + + private PropertyMapper getPropertyMapper(PropertySource source) { + if (source instanceof SystemEnvironmentPropertySource) { + return SystemEnvironmentPropertyMapper.INSTANCE; + } + return DefaultPropertyMapper.INSTANCE; + } + + /** + * Attach a {@link ConfigurationPropertySources} instance to the specified + * {@link ConfigurableEnvironment} so that classic + * {@link PropertySourcesPropertyResolver} calls will resolve using + * {@link ConfigurationPropertyName configuration property names}. + * @param environment the source environment + * @return the instance attached + */ + public static ConfigurationPropertySources attach( + ConfigurableEnvironment environment) { + return attach(environment.getPropertySources()); + } + + /** + * Attach a {@link ConfigurationPropertySources} instance to the specified + * {@link PropertySources} so that classic {@link PropertySourcesPropertyResolver} + * calls will resolve using using {@link ConfigurationPropertyName configuration + * property names}. + * @param propertySources the source property sources + * @return the instance attached + */ + public static ConfigurationPropertySources attach( + MutablePropertySources propertySources) { + ConfigurationPropertySources adapted = new ConfigurationPropertySources( + propertySources); + propertySources.addFirst(new ConfigurationPropertySourcesPropertySource( + PROPERTY_SOURCE_NAME, adapted)); + return adapted; + } + + /** + * Get a {@link ConfigurationPropertySources} instance for the specified + * {@link PropertySources} (either previously {@link #attach(MutablePropertySources) + * attached} or a new instance. + * @param propertySources the source property sources + * @return a {@link ConfigurationPropertySources} instance + */ + public static ConfigurationPropertySources get(PropertySources propertySources) { + if (propertySources == null) { + return null; + } + PropertySource source = propertySources.get(PROPERTY_SOURCE_NAME); + if (source != null) { + return (ConfigurationPropertySources) source.getSource(); + } + return new ConfigurationPropertySources(propertySources); + } + + /** + * Get a {@link ConfigurationPropertySources} instance for the {@link PropertySources} + * from the specified {@link ConfigurableEnvironment}, (either previously + * {@link #attach(MutablePropertySources) attached} or a new instance. + * @param environment the configurable environment + * @return a {@link ConfigurationPropertySources} instance + */ + public static ConfigurationPropertySources get(ConfigurableEnvironment environment) { + MutablePropertySources propertySources = environment.getPropertySources(); + PropertySource source = propertySources.get(PROPERTY_SOURCE_NAME); + if (source != null) { + return (ConfigurationPropertySources) source.getSource(); + } + return new ConfigurationPropertySources(propertySources); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java new file mode 100644 index 0000000000..eff2cb2469 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java @@ -0,0 +1,77 @@ +/* + * 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.Objects; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.PropertySource; + +/** + * {@link PropertySource} that exposes {@link ConfigurationPropertySource} instances so + * that they can be used with a {@link PropertyResolver} or added to the + * {@link Environment}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigurationPropertySourcesPropertySource + extends PropertySource> + implements OriginLookup { + + ConfigurationPropertySourcesPropertySource(String name, + Iterable source) { + super(name, source); + } + + @Override + public Object getProperty(String name) { + ConfigurationProperty configurationProperty = findConfigurationProperty(name); + return (configurationProperty == null ? null : configurationProperty.getValue()); + } + + @Override + public Origin getOrigin(String name) { + return Origin.from(findConfigurationProperty(name)); + } + + private ConfigurationProperty findConfigurationProperty(String name) { + try { + return findConfigurationProperty(ConfigurationPropertyName.of(name)); + } + catch (Exception ex) { + return null; + } + } + + private ConfigurationProperty findConfigurationProperty( + ConfigurationPropertyName name) { + if (name == null) { + return null; + } + Stream sources = StreamSupport + .stream(getSource().spliterator(), false); + return sources.map(source -> source.getConfigurationProperty(name)) + .filter(Objects::nonNull).findFirst().orElse(null); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java new file mode 100644 index 0000000000..0718e69c34 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java @@ -0,0 +1,106 @@ +/* + * 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.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.core.env.PropertySource; + +/** + * Default {@link PropertyMapper} implementation. Names are mapped by removing invalid + * characters and converting to lower case. For example "{@code my.server_name.PORT}" is + * mapped to "{@code my.servername.port}". + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertyMapper + * @see PropertySourceConfigurationPropertySource + */ +class DefaultPropertyMapper implements PropertyMapper { + + public static final PropertyMapper INSTANCE = new DefaultPropertyMapper(); + + private Cache configurationPropertySourceCache = new Cache<>(); + + private Cache propertySourceCache = new Cache<>(); + + private final ConfigurationPropertyNameBuilder nameBuilder = new ConfigurationPropertyNameBuilder(); + + @Override + public List map(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName) { + List mapping = this.configurationPropertySourceCache + .get(configurationPropertyName); + if (mapping == null) { + String convertedName = configurationPropertyName.toString(); + mapping = Collections.singletonList( + new PropertyMapping(convertedName, configurationPropertyName)); + } + return mapping; + } + + @Override + public List map(PropertySource propertySource, + String propertySourceName) { + List mapping = this.propertySourceCache.get(propertySourceName); + if (mapping == null) { + mapping = tryMap(propertySourceName); + this.propertySourceCache.put(propertySourceName, mapping); + } + return mapping; + } + + private List tryMap(String propertySourceName) { + try { + ConfigurationPropertyName convertedName = this.nameBuilder + .from(propertySourceName, '.').build(); + PropertyMapping o = new PropertyMapping(propertySourceName, convertedName); + return Collections.singletonList(o); + } + catch (Exception ex) { + return Collections.emptyList(); + } + } + + private static class Cache extends LinkedHashMap> { + + private final int capacity; + + Cache() { + this(1); + } + + Cache(int capacity) { + super(capacity, (float) 0.75, true); + this.capacity = capacity; + } + + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + if (size() < this.capacity) { + return false; + } + return true; + + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSource.java new file mode 100644 index 0000000000..407bec986c --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSource.java @@ -0,0 +1,57 @@ +/* + * 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.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.util.Assert; + +/** + * A filtered {@link ConfigurationPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class FilteredConfigurationPropertiesSource implements ConfigurationPropertySource { + + private final ConfigurationPropertySource source; + + private final Predicate filter; + + FilteredConfigurationPropertiesSource(ConfigurationPropertySource source, + Predicate filter) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(filter, "Filter must not be null"); + this.source = source; + this.filter = filter; + } + + @Override + public Stream stream() { + return StreamSupport.stream(this.source.spliterator(), false).filter(this.filter); + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + return (this.filter.test(name) ? this.source.getConfigurationProperty(name) + : null); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java new file mode 100644 index 0000000000..d659e540f2 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java @@ -0,0 +1,96 @@ +/* + * 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.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.util.Assert; + +/** + * An {@link ConfigurationPropertySource} backed by a {@link Map} and using standard name + * mapping rules. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.0.0 + */ +public class MapConfigurationPropertySource implements ConfigurationPropertySource { + + private final Map source; + + private final ConfigurationPropertySource delegate; + + /** + * Create a new empty {@link MapConfigurationPropertySource} instance. + */ + public MapConfigurationPropertySource() { + this(Collections.emptyMap()); + } + + /** + * Create a new {@link MapConfigurationPropertySource} instance with entries copies + * from the specified map. + * @param map the source map + */ + public MapConfigurationPropertySource(Map map) { + this.source = new LinkedHashMap<>(); + this.delegate = new PropertySourceConfigurationPropertySource( + new MapPropertySource("source", this.source), + new DefaultPropertyMapper()); + putAll(map); + } + + /** + * Add all enties from the specified map. + * @param map the source map + */ + public void putAll(Map map) { + Assert.notNull(map, "Map must not be null"); + map.forEach(this::put); + } + + /** + * Add an individual entry. + * @param name the name + * @param value the value + */ + public void put(Object name, Object value) { + this.source.put((name == null ? null : name.toString()), value); + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + return this.delegate.getConfigurationProperty(name); + } + + @Override + public Iterator iterator() { + return this.delegate.iterator(); + } + + @Override + public Stream stream() { + return this.delegate.stream(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java new file mode 100644 index 0000000000..c1d5d8164e --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java @@ -0,0 +1,62 @@ +/* + * 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.List; + +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; + +/** + * Strategy used to provide a mapping between a {@link PropertySource} and a + * {@link ConfigurationPropertySource}. + *

+ * Mappings should be provided for both {@link ConfigurationPropertyName + * ConfigurationPropertyName} types and {@code String} based names. This allows the + * {@link PropertySourceConfigurationPropertySource} 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 PropertySourceConfigurationPropertySource} for more details. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertySourceConfigurationPropertySource + */ +interface PropertyMapper { + + /** + * Provide mappings from a {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName}. + * @param propertySource the property source + * @param configurationPropertyName the name to map + * @return a stream of mappings or {@code Stream#empty()} + */ + List map(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName); + + /** + * Provide mappings from a {@link PropertySource} property name. + * @param propertySource the property source + * @param propertySourceName the name to map + * @return a stream of mappings or {@code Stream#empty()} + */ + List map(PropertySource propertySource, + String propertySourceName); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapping.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapping.java new file mode 100644 index 0000000000..be60439282 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapping.java @@ -0,0 +1,102 @@ +/* + * 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.function.Function; + +import org.springframework.core.env.PropertySource; + +/** + * Details a mapping between a {@link PropertySource} item and a + * {@link ConfigurationPropertySource} item. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertySourceConfigurationPropertySource + */ +class PropertyMapping { + + private final String propertySourceName; + + private final ConfigurationPropertyName configurationPropertyName; + + private final Function valueExtractor; + + /** + * Create a new {@link PropertyMapper} instance. + * @param propertySourceName the {@link PropertySource} name + * @param configurationPropertyName the {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName} + */ + PropertyMapping(String propertySourceName, + ConfigurationPropertyName configurationPropertyName) { + this(propertySourceName, configurationPropertyName, Function.identity()); + } + + /** + * Create a new {@link PropertyMapper} instance. + * @param propertySourceName the {@link PropertySource} name + * @param configurationPropertyName the {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName} + * @param valueExtractor the extractor used to obtain the value + */ + PropertyMapping(String propertySourceName, + ConfigurationPropertyName configurationPropertyName, + Function valueExtractor) { + this.propertySourceName = propertySourceName; + this.configurationPropertyName = configurationPropertyName; + this.valueExtractor = valueExtractor; + } + + /** + * Return the mapped {@link PropertySource} name. + * @return the property source name (never {@code null}) + */ + public String getPropertySourceName() { + return this.propertySourceName; + + } + + /** + * Return the mapped {@link ConfigurationPropertySource} + * {@link ConfigurationPropertyName}. + * @return the configuration property source name (never {@code null}) + */ + public ConfigurationPropertyName getConfigurationPropertyName() { + return this.configurationPropertyName; + + } + + /** + * Return a function that can be used to extract the {@link PropertySource} value. + * @return the value extractor (never {@code null}) + */ + public Function getValueExtractor() { + return this.valueExtractor; + } + + /** + * Return if this mapping is applicable for the given + * {@link ConfigurationPropertyName}. + * @param name the name to check + * @return if the mapping is applicable + */ + public boolean isApplicable(ConfigurationPropertyName name) { + return this.configurationPropertyName.equals(name); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySource.java new file mode 100644 index 0000000000..696ea9668f --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySource.java @@ -0,0 +1,265 @@ +/* + * 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.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.PropertySourceOrigin; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link ConfigurationPropertySource} backed by a Spring {@link PropertySource}. Provides + * support for {@link EnumerablePropertySource} when possible but can also be used to + * non-enumerable property sources or restricted {@link EnumerablePropertySource} + * implementation (such as a security restricted {@code systemEnvironment} source). A + * {@link PropertySource} is adapted with the help of a {@link PropertyMapper} which + * provides the mapping rules for individual properties. + *

+ * Each + * {@link ConfigurationPropertySource#getConfigurationProperty(ConfigurationPropertyName) + * getValue} call initially attempts to + * {@link PropertyMapper#map(PropertySource, ConfigurationPropertyName) map} the + * {@link ConfigurationPropertyName} to one or more {@code String} based names. This + * allows fast property resolution for well formed property sources and allows the adapter + * to work with non {@link EnumerablePropertySource enumerable property sources}. + *

+ * If direct {@link ConfigurationPropertyName} to {@code String} mapping is unsuccessful a + * brute force approach is taken by {@link EnumerablePropertySource#getPropertyNames() + * enumerating} known {@code String} {@link PropertySource} names, mapping them to one or + * more {@link ConfigurationPropertyName} and checking for + * {@link PropertyMapping#isApplicable(ConfigurationPropertyName) applicability}. The + * enumeration approach supports property sources where it isn't practical to guess all + * direct mapping combinations. + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertyMapper + */ +class PropertySourceConfigurationPropertySource implements ConfigurationPropertySource { + + private final PropertySource propertySource; + + private final PropertyMapper mapper; + + private volatile Object cacheKey; + + private volatile Cache cache; + + /** + * Create a new {@link PropertySourceConfigurationPropertySource} implementation. + * @param propertySource the source property source + * @param mapper the property mapper + */ + PropertySourceConfigurationPropertySource(PropertySource propertySource, + PropertyMapper mapper) { + Assert.notNull(propertySource, "PropertySource must not be null"); + Assert.notNull(mapper, "Mapper must not be null"); + this.propertySource = propertySource; + this.mapper = new ExceptionSwallowingPropertyMapper(mapper); + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + ConfigurationProperty configurationProperty = findDirectly(name); + if (configurationProperty == null) { + configurationProperty = findByEnumeration(name); + } + return configurationProperty; + } + + private ConfigurationProperty findDirectly(ConfigurationPropertyName name) { + List mappings = this.mapper.map(this.propertySource, name); + return find(mappings, name); + } + + private ConfigurationProperty findByEnumeration(ConfigurationPropertyName name) { + List mappings = getPropertyMappings(); + return find(mappings, name); + } + + private ConfigurationProperty find(List mappings, + ConfigurationPropertyName name) { + // Use for-loops rather than streams since this method is called often + for (PropertyMapping mapping : mappings) { + if (mapping.isApplicable(name)) { + ConfigurationProperty property = find(mapping); + if (property != null) { + return property; + } + } + } + return null; + } + + private ConfigurationProperty find(PropertyMapping mapping) { + String propertySourceName = mapping.getPropertySourceName(); + Object value = this.propertySource.getProperty(propertySourceName); + if (value == null) { + return null; + } + value = mapping.getValueExtractor().apply(value); + ConfigurationPropertyName configurationPropertyName = mapping + .getConfigurationPropertyName(); + Origin origin = PropertySourceOrigin.get(this.propertySource, propertySourceName); + return ConfigurationProperty.of(configurationPropertyName, value, origin); + } + + @Override + public Stream stream() { + return getConfigurationPropertyNames().stream(); + } + + @Override + public Iterator iterator() { + return getConfigurationPropertyNames().iterator(); + } + + private List getConfigurationPropertyNames() { + Cache cache = getCache(); + List names = (cache != null ? cache.getNames() : null); + if (names != null) { + return names; + } + List mappings = getPropertyMappings(); + names = new ArrayList(mappings.size()); + for (PropertyMapping mapping : mappings) { + names.add(mapping.getConfigurationPropertyName()); + } + names = Collections.unmodifiableList(names); + if (cache != null) { + cache.setNames(names); + } + return names; + } + + private List getPropertyMappings() { + if (!(this.propertySource instanceof EnumerablePropertySource)) { + return Collections.emptyList(); + } + Cache cache = getCache(); + List mappings = (cache != null ? cache.getMappings() : null); + if (mappings != null) { + return mappings; + } + String[] names = ((EnumerablePropertySource) this.propertySource) + .getPropertyNames(); + mappings = new ArrayList(names.length); + for (String name : names) { + mappings.addAll(this.mapper.map(this.propertySource, name)); + } + mappings = Collections.unmodifiableList(mappings); + if (cache != null) { + cache.setMappings(mappings); + } + return mappings; + } + + private Cache getCache() { + Object cacheKey = getCacheKey(); + if (cacheKey == null) { + return null; + } + if (ObjectUtils.nullSafeEquals(cacheKey, this.cacheKey)) { + return this.cache; + } + this.cache = new Cache(); + this.cacheKey = cacheKey; + return this.cache; + } + + private Object getCacheKey() { + if (this.propertySource instanceof MapPropertySource) { + return ((MapPropertySource) this.propertySource).getSource().keySet(); + } + if (this.propertySource instanceof EnumerablePropertySource) { + return ((EnumerablePropertySource) this.propertySource).getPropertyNames(); + } + return null; + } + + /** + * {@link PropertyMapper} that swallows exceptions when the mapping fails. + */ + private static class ExceptionSwallowingPropertyMapper implements PropertyMapper { + + private final PropertyMapper mapper; + + ExceptionSwallowingPropertyMapper(PropertyMapper mapper) { + this.mapper = mapper; + } + + @Override + public List map(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName) { + try { + return this.mapper.map(propertySource, configurationPropertyName); + } + catch (Exception ex) { + return Collections.emptyList(); + } + } + + @Override + public List map(PropertySource propertySource, + String propertySourceName) { + try { + return this.mapper.map(propertySource, propertySourceName); + } + catch (Exception ex) { + return Collections.emptyList(); + } + } + + } + + private static class Cache { + + private ConfigurationPropertyName knownMissingName; + + private List names; + + private List mappings; + + public List getNames() { + return this.names; + } + + public void setNames(List names) { + this.names = names; + } + + public List getMappings() { + return this.mappings; + } + + public void setMappings(List mappings) { + this.mappings = mappings; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java new file mode 100644 index 0000000000..c95e56ecca --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java @@ -0,0 +1,164 @@ +/* + * 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.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; +import org.springframework.core.env.PropertySource; +import org.springframework.util.StringUtils; + +/** + * {@link PropertyMapper} for system environment variables. Names are mapped by removing + * invalid characters, converting to lower case and replacing "{@code _}" with + * "{@code .}". For example, "{@code SERVER_PORT}" is mapped to "{@code server.port}". In + * addition, numeric elements are mapped to indexes (e.g. "{@code HOST_0}" is mapped to + * "{@code host[0]}"). + *

+ * List shortcuts (names that end with double underscore) are also supported by this + * mapper. For example, "{@code MY_LIST__=a,b,c}" is mapped to "{@code my.list[0]=a}", + * "{@code my.list[1]=b}" ,"{@code my.list[2]=c}". + * + * @author Phillip Webb + * @author Madhura Bhave + * @see PropertyMapper + * @see PropertySourceConfigurationPropertySource + */ +class SystemEnvironmentPropertyMapper implements PropertyMapper { + + public static PropertyMapper INSTANCE = new SystemEnvironmentPropertyMapper(); + + private final ConfigurationPropertyNameBuilder nameBuilder = new ConfigurationPropertyNameBuilder( + this::createElement); + + @Override + public List map(PropertySource propertySource, + String propertySourceName) { + ConfigurationPropertyName name = convertName(propertySourceName); + if (name == null) { + return Collections.emptyList(); + } + if (propertySourceName.endsWith("__")) { + return expandListShortcut(propertySourceName, name, + propertySource.getProperty(propertySourceName)); + } + return Collections.singletonList(new PropertyMapping(propertySourceName, name)); + } + + private ConfigurationPropertyName convertName(String propertySourceName) { + try { + return this.nameBuilder.from(propertySourceName, '_').build(); + } + catch (Exception ex) { + return null; + } + } + + private List expandListShortcut(String propertySourceName, + ConfigurationPropertyName rootName, Object value) { + if (value == null) { + return Collections.emptyList(); + } + List mappings = new ArrayList<>(); + String[] elements = StringUtils + .commaDelimitedListToStringArray(String.valueOf(value)); + for (int i = 0; i < elements.length; i++) { + ConfigurationPropertyName name = ConfigurationPropertyName + .of(rootName.toString() + "[" + i + "]"); + mappings.add(new PropertyMapping(propertySourceName, name, + new ElementExtractor(i))); + } + return mappings; + } + + @Override + public List map(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName) { + String name = convertName(configurationPropertyName); + List result = Collections + .singletonList(new PropertyMapping(name, configurationPropertyName)); + if (isListShortcutPossible(configurationPropertyName)) { + result = new ArrayList<>(result); + result.addAll(mapListShortcut(propertySource, configurationPropertyName)); + } + return result; + } + + private String convertName(ConfigurationPropertyName configurationPropertyName) { + String propertyName = configurationPropertyName.stream() + .map(name -> name.getValue(Form.UNIFORM).toUpperCase()) + .collect(Collectors.joining("_")); + return propertyName; + } + + private boolean isListShortcutPossible(ConfigurationPropertyName name) { + return (name.getElement().isIndexed() + && isNumber(name.getElement().getValue(Form.UNIFORM)) + && name.getParent() != null); + } + + private List mapListShortcut(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName) { + String propertyName = convertName(configurationPropertyName.getParent()) + "__"; + if (propertySource.containsProperty(propertyName)) { + int index = Integer.parseInt( + configurationPropertyName.getElement().getValue(Form.UNIFORM)); + return Collections.singletonList(new PropertyMapping(propertyName, + configurationPropertyName, new ElementExtractor(index))); + } + return Collections.emptyList(); + } + + private String createElement(String value) { + value = value.toLowerCase(); + return (isNumber(value) ? "[" + value + "]" : value); + } + + private static boolean isNumber(String string) { + IntStream nonDigits = string.chars().filter((c) -> !Character.isDigit(c)); + boolean hasNonDigit = nonDigits.findFirst().isPresent(); + return !hasNonDigit; + } + + /** + * Function used to extract an element from a comma list. + */ + private static class ElementExtractor implements Function { + + private final int index; + + ElementExtractor(int index) { + this.index = index; + } + + @Override + public Object apply(Object value) { + if (value == null) { + return null; + } + return StringUtils + .commaDelimitedListToStringArray(value.toString())[this.index]; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/source/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/package-info.java new file mode 100644 index 0000000000..4d0169c7d5 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/source/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * Sources for external configuration properties. + * + * @see org.springframework.boot.context.properties.source.ConfigurationPropertySource + */ +package org.springframework.boot.context.properties.source; diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java new file mode 100644 index 0000000000..2f4c8b72b1 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java @@ -0,0 +1,58 @@ +/* + * 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.Collections; +import java.util.Iterator; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; + +/** + * Abstract base class for {@link PropertyMapper} tests. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public abstract class AbstractPropertyMapperTests { + + protected abstract PropertyMapper getMapper(); + + protected final Iterator namesFromString(String name) { + return namesFromString(name, "value"); + } + + protected final Iterator namesFromString(String name, Object value) { + PropertySource propertySource = new MapPropertySource("test", + Collections.singletonMap(name, value)); + return getMapper().map(propertySource, name).stream() + .map((mapping) -> mapping.getConfigurationPropertyName().toString()) + .iterator(); + } + + protected final Iterator namesFromConfiguration(String name) { + return namesFromConfiguration(name, "value"); + } + + protected final Iterator namesFromConfiguration(String name, String value) { + PropertySource propertySource = new MapPropertySource("test", + Collections.singletonMap(name, value)); + return getMapper().map(propertySource, ConfigurationPropertyName.of(name)) + .stream().map((mapping) -> mapping.getPropertySourceName()).iterator(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySourceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySourceTests.java new file mode 100644 index 0000000000..8fc13652dd --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AliasedConfigurationPropertySourceTests.java @@ -0,0 +1,72 @@ +/* + * 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 org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AliasedConfigurationPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class AliasedConfigurationPropertySourceTests { + + @Test + public void streamShouldInclueAliases() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "bing"); + source.put("foo.baz", "biff"); + ConfigurationPropertySource aliased = source + .withAliases(new ConfigurationPropertyNameAliases("foo.bar", "foo.bar1")); + assertThat(aliased.stream()).containsExactly( + ConfigurationPropertyName.of("foo.bar"), + ConfigurationPropertyName.of("foo.bar1"), + ConfigurationPropertyName.of("foo.baz")); + } + + @Test + public void getConfigurationPropertyShouldConsiderAliases() throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "bing"); + source.put("foo.baz", "biff"); + ConfigurationPropertySource aliased = source + .withAliases(new ConfigurationPropertyNameAliases("foo.bar", "foo.bar1")); + assertThat(getValue(aliased, "foo.bar")).isEqualTo("bing"); + assertThat(getValue(aliased, "foo.bar1")).isEqualTo("bing"); + } + + @Test + public void getConfigurationPropertyWhenNotAliasesShouldReturnValue() + throws Exception { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.bar", "bing"); + source.put("foo.baz", "biff"); + ConfigurationPropertySource aliased = source + .withAliases(new ConfigurationPropertyNameAliases("foo.bar", "foo.bar1")); + assertThat(getValue(aliased, "foo.baz")).isEqualTo("biff"); + } + + private Object getValue(ConfigurationPropertySource source, String name) { + ConfigurationProperty property = source + .getConfigurationProperty(ConfigurationPropertyName.of(name)); + return (property == null ? null : property.getValue()); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliasesTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliasesTests.java new file mode 100644 index 0000000000..c68b303a90 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameAliasesTests.java @@ -0,0 +1,124 @@ +/* + * 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 org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertyNameAliases}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertyNameAliasesTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWithStringWhenNullNameShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be null"); + new ConfigurationPropertyNameAliases((String) null); + } + + @Test + public void createWithStringShouldAddMapping() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases( + "foo", "bar", "baz"); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + } + + @Test + public void createWithNameShouldAddMapping() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases( + ConfigurationPropertyName.of("foo"), ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + } + + @Test + public void addAliasesFromStringShouldAddMapping() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("foo", "bar", "baz"); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + } + + @Test + public void addAlaisesFromNameShouldAddMapping() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises(ConfigurationPropertyName.of("foo"), + ConfigurationPropertyName.of("bar"), ConfigurationPropertyName.of("baz")); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + } + + @Test + public void addWhenHasExistingShouldAddAdditionalMappings() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("foo", "bar"); + aliases.addAlaises("foo", "baz"); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar"), + ConfigurationPropertyName.of("baz")); + } + + @Test + public void getAliasesWhenNotMappedShouldReturnEmptyList() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))).isEmpty(); + } + + @Test + public void getAliasesWhenMappedShouldReturnMapping() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("foo", "bar"); + assertThat(aliases.getAliases(ConfigurationPropertyName.of("foo"))) + .containsExactly(ConfigurationPropertyName.of("bar")); + } + + @Test + public void getNameForAliasWhenHasMappingShouldReturnName() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("foo", "bar"); + aliases.addAlaises("foo", "baz"); + assertThat((Object) aliases.getNameForAlias(ConfigurationPropertyName.of("bar"))) + .isEqualTo(ConfigurationPropertyName.of("foo")); + assertThat((Object) aliases.getNameForAlias(ConfigurationPropertyName.of("baz"))) + .isEqualTo(ConfigurationPropertyName.of("foo")); + } + + @Test + public void getNameForAliasWhenNotMappedShouldReturnNull() throws Exception { + ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); + aliases.addAlaises("foo", "bar"); + assertThat((Object) aliases.getNameForAlias(ConfigurationPropertyName.of("baz"))) + .isNull(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilderTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilderTests.java new file mode 100644 index 0000000000..9ccba0c0d9 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameBuilderTests.java @@ -0,0 +1,138 @@ +/* + * 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.Arrays; +import java.util.List; +import java.util.regex.Pattern; +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 org.springframework.test.util.ReflectionTestUtils; + +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 createWhenPatternIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Pattern must not be null"); + this.builder = new ConfigurationPropertyNameBuilder((Pattern) null); + } + + @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", '.').build(); + assertThat(name.toString()).isEqualTo(expected.toString()); + } + + @Test + public void buildShouldValidateUsingPattern() { + Pattern pattern = Pattern.compile("[a-z]([a-z0-9\\-])*"); + this.builder = new ConfigurationPropertyNameBuilder(pattern); + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Element value 'foo@!' is not valid"); + this.builder.from("foo@!.bar", '.').build(); + } + + @Test + public void buildWhenHasNoElementsShouldThrowException() throws Exception { + this.builder = new ConfigurationPropertyNameBuilder(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("At least one element must be defined"); + this.builder.build(); + } + + @Test + public void buildShouldUseElementProcessor() throws Exception { + this.builder = new ConfigurationPropertyNameBuilder( + value -> value.replace("-", "")); + ConfigurationPropertyName name = this.builder.from("FOO_THE-BAR", '_').build(); + assertThat(name.toString()).isEqualTo("foo.thebar"); + } + + @Test + public void fromNameShouldSetElements() throws Exception { + this.builder = new ConfigurationPropertyNameBuilder(); + ConfigurationPropertyName name = this.builder.from("foo.bar", '.').build(); + 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 fromNameWhenHasExistingShouldSetNewElements() throws Exception { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Existing elements must not be present"); + new ConfigurationPropertyNameBuilder().from("foo.bar", '.').from("baz", '.') + .build(); + } + + @Test + public void appendShouldAppendElement() throws Exception { + this.builder = new ConfigurationPropertyNameBuilder(); + ConfigurationPropertyName name = this.builder.from("foo.bar", '.').append("baz") + .build(); + assertThat(name.toString()).isEqualTo("foo.bar.baz"); + } + + private List elements(String... elements) { + return Arrays.stream(elements).map(Element::new).collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") + private List getElements(String name) { + ConfigurationPropertyNameBuilder builder = this.builder.from(name, '.'); + return (List) ReflectionTestUtils.getField(builder, "elements"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java new file mode 100644 index 0000000000..a0df87b141 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java @@ -0,0 +1,326 @@ +/* + * 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.Collections; +import java.util.Iterator; +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.ConfigurationPropertyName.Form; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link ConfigurationPropertyName}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertyNameTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void ofNameShouldNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be null"); + ConfigurationPropertyName.of((String) null); + } + + @Test + public void ofNameShouldNotStartWithNumber() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("1foo"); + } + + @Test + public void ofNameShouldNotStartWithDash() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("-foo"); + } + + @Test + public void ofNameShouldNotStartWithDot() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not start with '.'"); + ConfigurationPropertyName.of(".foo"); + } + + @Test + public void ofNameShouldNotEndWithDot() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not end with '.'"); + ConfigurationPropertyName.of("foo."); + } + + @Test + public void ofNameShouldNotContainUppercase() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("fOo"); + } + + @Test + public void ofNameShouldNotContainInvalidChars() throws Exception { + String invalid = "_@$%*+=':;"; + for (char c : invalid.toCharArray()) { + try { + ConfigurationPropertyName.of("foo" + c); + fail("Did not throw for invalid char " + c); + } + catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).contains("is not valid"); + } + } + } + + @Test + 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"); + } + + @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]"); + } + + @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"); + } + + @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"); + } + + @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"); + } + + @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"); + } + + @Test + public void ofNameWhenMissingCloseBracket() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("[bar"); + } + + @Test + public void ofNameWhenMissingOpenBracket() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("bar]"); + } + + @Test + public void ofNameWithWhitespaceInName() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("is not valid"); + ConfigurationPropertyName.of("foo. bar"); + } + + @Test + 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"); + } + + @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"); + } + + @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"); + } + + @Test + public void elementNameShouldNotIncludeDashes() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("f-o-o"); + assertThat(name.getElement().getValue(Form.UNIFORM)).isEqualTo("foo"); + } + + @Test + public void streamShouldReturnElements() throws Exception { + assertThat(streamElements("foo.bar")).containsExactly("foo", "bar"); + assertThat(streamElements("foo[0]")).containsExactly("foo", "[0]"); + assertThat(streamElements("foo.[0]")).containsExactly("foo", "[0]"); + assertThat(streamElements("foo[baz]")).containsExactly("foo", "[baz]"); + assertThat(streamElements("foo.baz")).containsExactly("foo", "baz"); + assertThat(streamElements("foo[baz].bar")).containsExactly("foo", "[baz]", "bar"); + assertThat(streamElements("foo.baz.bar")).containsExactly("foo", "baz", "bar"); + assertThat(streamElements("foo.baz-bar")).containsExactly("foo", "baz-bar"); + } + + private Iterator streamElements(String name) { + return ConfigurationPropertyName.of(name).stream().map((e) -> e.toString()) + .iterator(); + } + + @Test + public void elementIsIndexedWhenIndexedShouldReturnTrue() throws Exception { + assertThat(ConfigurationPropertyName.of("foo[0]").getElement().isIndexed()) + .isTrue(); + } + + @Test + public void elementIsIndexedWhenNotIndexedShouldReturnFalse() throws Exception { + assertThat(ConfigurationPropertyName.of("foo.bar").getElement().isIndexed()) + .isFalse(); + } + + @Test + public void isAncestorOfWhenSameShouldReturnFalse() throws Exception { + ConfigurationPropertyName parent = ConfigurationPropertyName.of("foo"); + assertThat(parent.isAncestorOf(parent)).isFalse(); + } + + @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(); + } + + @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(); + } + + @Test + public void appendWhenNotIndexedShouldAppendWithDot() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + assertThat(name.append("bar").toString()).isEqualTo("foo.bar"); + } + + @Test + public void appendWhenIndexedShouldAppendWithBrackets() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo") + .append("[bar]"); + assertThat(name.getElement().isIndexed()).isTrue(); + assertThat(name.toString()).isEqualTo("foo[bar]"); + } + + @Test + public void appendWhenElementNameIsNotValidShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Element value '1bar' is not valid"); + ConfigurationPropertyName.of("foo").append("1bar"); + } + + @Test + public void appendWhenElementNameIsNullShouldReturnName() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + assertThat((Object) name.append((String) null)).isSameAs(name); + } + + @Test + public void compareShouldSortNames() throws Exception { + List names = new ArrayList<>(); + names.add(ConfigurationPropertyName.of("foo[10]")); + names.add(ConfigurationPropertyName.of("foo.bard")); + names.add(ConfigurationPropertyName.of("foo[2]")); + names.add(ConfigurationPropertyName.of("foo.bar")); + names.add(ConfigurationPropertyName.of("foo.baz")); + names.add(ConfigurationPropertyName.of("foo")); + Collections.sort(names); + assertThat(names.stream().map(ConfigurationPropertyName::toString) + .collect(Collectors.toList())).containsExactly("foo", "foo[2]", "foo[10]", + "foo.bar", "foo.bard", "foo.baz"); + } + + @Test + public void ofNameCanBeEmpty() throws Exception { + ConfigurationPropertyName name = ConfigurationPropertyName.of(""); + assertThat(name.toString()).isEqualTo(""); + assertThat(name.append("foo").toString()).isEqualTo("foo"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySourceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySourceTests.java new file mode 100644 index 0000000000..b43a91fb25 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySourceTests.java @@ -0,0 +1,96 @@ +/* + * 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.List; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertySourcesPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertySourcesPropertySourceTests { + + private List configurationSources = new ArrayList<>(); + + private ConfigurationPropertySourcesPropertySource propertySource = new ConfigurationPropertySourcesPropertySource( + "test", this.configurationSources); + + @Test + public void getPropertyShouldReturnValue() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz")); + assertThat(this.propertySource.getProperty("foo.bar")).isEqualTo("baz"); + } + + @Test + public void getPropertyWhenNameIsNotValidShouldReturnNull() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz")); + assertThat(this.propertySource.getProperty("FOO.B-A-R")).isNull(); + assertThat(this.propertySource.getProperty("FOO.B A R")).isNull(); + assertThat(this.propertySource.getProperty(".foo.bar")).isNull(); + } + + @Test + public void getPropertyWhenMultipleShouldReturnFirst() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz")); + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "bill")); + assertThat(this.propertySource.getProperty("foo.bar")).isEqualTo("baz"); + } + + @Test + public void getPropertyWhenNoneShouldReturnFirst() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz")); + assertThat(this.propertySource.getProperty("foo.foo")).isNull(); + } + + @Test + public void getPropertyOriginShouldReturnOrigin() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz", "line1")); + assertThat(this.propertySource.getOrigin("foo.bar").toString()) + .isEqualTo("line1"); + } + + @Test + public void getPropertyOriginWhenMissingShouldReturnNull() throws Exception { + this.configurationSources + .add(new MockConfigurationPropertySource("foo.bar", "baz", "line1")); + assertThat(this.propertySource.getOrigin("foo.foo")).isNull(); + } + + @Test + public void getNameShouldReturnName() throws Exception { + assertThat(this.propertySource.getName()).isEqualTo("test"); + } + + @Test + public void getSourceShouldReturnSource() throws Exception { + assertThat(this.propertySource.getSource()).isSameAs(this.configurationSources); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java new file mode 100644 index 0000000000..554600c23b --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java @@ -0,0 +1,180 @@ +/* + * 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.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySourcesPropertyResolver; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertySources}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertySourcesTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenPropertySourcesIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("PropertySources must not be null"); + new ConfigurationPropertySources(null); + } + + @Test + public void iteratorShouldAdaptPropertySource() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new MapPropertySource("test", + Collections.singletonMap("a", "b"))); + Iterator iterator = new ConfigurationPropertySources( + sources).iterator(); + assertThat(iterator.next() + .getConfigurationProperty(ConfigurationPropertyName.of("a")).getValue()) + .isEqualTo("b"); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + public void iteratorShouldAdaptSystemEnvironmentPropertySource() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addLast(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("SERVER_PORT", "1234"))); + Iterator iterator = new ConfigurationPropertySources( + sources).iterator(); + assertThat( + iterator.next() + .getConfigurationProperty( + ConfigurationPropertyName.of("server.port")) + .getValue()).isEqualTo("1234"); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + public void iteratorShouldAdaptMultiplePropertySources() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addLast(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("SERVER_PORT", "1234"))); + sources.addLast(new MapPropertySource("test1", + Collections.singletonMap("server.po-rt", "4567"))); + sources.addLast(new MapPropertySource("test2", + Collections.singletonMap("a", "b"))); + Iterator iterator = new ConfigurationPropertySources( + sources).iterator(); + assertThat( + iterator.next() + .getConfigurationProperty( + ConfigurationPropertyName.of("server.port")) + .getValue()).isEqualTo("1234"); + assertThat( + iterator.next() + .getConfigurationProperty( + ConfigurationPropertyName.of("server.port")) + .getValue()).isEqualTo("4567"); + assertThat(iterator.next() + .getConfigurationProperty(ConfigurationPropertyName.of("a")).getValue()) + .isEqualTo("b"); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + public void attachShouldAddAdapterAtBegining() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addLast(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("SERVER_PORT", "1234"))); + sources.addLast(new MapPropertySource("config", + Collections.singletonMap("server.port", "4568"))); + assertThat(sources.size()).isEqualTo(2); + ConfigurationPropertySources.attach(sources); + PropertyResolver resolver = new PropertySourcesPropertyResolver(sources); + assertThat(resolver.getProperty("server.port")).isEqualTo("1234"); + assertThat(sources.size()).isEqualTo(3); + } + + @Test + public void getWhenAttachedShouldReturnAttached() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new MapPropertySource("test", + Collections.singletonMap("a", "b"))); + ConfigurationPropertySources attached = ConfigurationPropertySources + .attach(sources); + assertThat(ConfigurationPropertySources.get(sources)).isSameAs(attached); + } + + @Test + public void getWhenNotAttachedShouldReturnNew() throws Exception { + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new MapPropertySource("test", + Collections.singletonMap("a", "b"))); + assertThat(ConfigurationPropertySources.get(sources)).isNotNull(); + assertThat(sources.size()).isEqualTo(1); + } + + @Test + public void environmentProperyExpansionShouldWorkWhenAttached() throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + Map source = new LinkedHashMap<>(); + source.put("fooBar", "Spring ${barBaz} ${bar-baz}"); + source.put("barBaz", "Boot"); + PropertySource propertySource = new MapPropertySource("test", source); + environment.getPropertySources().addFirst(propertySource); + ConfigurationPropertySources.attach(environment); + assertThat(environment.getProperty("foo-bar")).isEqualTo("Spring Boot Boot"); + } + + @Test + public void environmentSourceShouldBeFlattened() throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("foo", + Collections.singletonMap("foo", "bar"))); + environment.getPropertySources().addFirst(new MapPropertySource("far", + Collections.singletonMap("far", "far"))); + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new PropertySource("env", environment) { + + @Override + public String getProperty(String key) { + return this.source.getProperty(key); + } + + }); + sources.addLast(new MapPropertySource("baz", + Collections.singletonMap("baz", "barf"))); + ConfigurationPropertySources configurationSources = ConfigurationPropertySources + .get(sources); + assertThat(configurationSources.iterator()).hasSize(5); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyTests.java new file mode 100644 index 0000000000..5e84e3e1e3 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyTests.java @@ -0,0 +1,97 @@ +/* + * 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 org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ConfigurationProperty}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class ConfigurationPropertyTests { + + private static final ConfigurationPropertyName NAME = ConfigurationPropertyName + .of("foo"); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenNameIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be null"); + new ConfigurationProperty(null, "bar", null); + } + + @Test + public void createWhenValueIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Value must not be null"); + new ConfigurationProperty(NAME, null, null); + } + + @Test + public void getNameShouldReturnName() throws Exception { + ConfigurationProperty property = ConfigurationProperty.of(NAME, "foo", null); + assertThat((Object) property.getName()).isEqualTo(NAME); + } + + @Test + public void getValueShouldReturnValue() throws Exception { + ConfigurationProperty property = ConfigurationProperty.of(NAME, "foo", null); + assertThat(property.getValue()).isEqualTo("foo"); + } + + @Test + public void getPropertyOrginShouldReturnValuePropertyOrigin() throws Exception { + Origin origin = mock(Origin.class); + OriginProvider property = ConfigurationProperty.of(NAME, "foo", origin); + assertThat(property.getOrigin()).isEqualTo(origin); + } + + @Test + public void equalsAndHashCode() throws Exception { + ConfigurationProperty property1 = new ConfigurationProperty( + ConfigurationPropertyName.of("foo"), "bar", null); + ConfigurationProperty property2 = new ConfigurationProperty( + ConfigurationPropertyName.of("foo"), "bar", null); + ConfigurationProperty property3 = new ConfigurationProperty( + ConfigurationPropertyName.of("foo"), "baz", null); + ConfigurationProperty property4 = new ConfigurationProperty( + ConfigurationPropertyName.of("baz"), "bar", null); + assertThat(property1.hashCode()).isEqualTo(property2.hashCode()); + assertThat(property1).isEqualTo(property2).isNotEqualTo(property3) + .isNotEqualTo(property4); + } + + @Test + public void toStringShouldReturnValue() throws Exception { + ConfigurationProperty property = ConfigurationProperty.of(NAME, "foo", null); + assertThat(property.toString()).contains("name").contains("value"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java new file mode 100644 index 0000000000..c03b0968b3 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java @@ -0,0 +1,70 @@ +/* + * 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 org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultPropertyMapper}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class DefaultPropertyMapperTests extends AbstractPropertyMapperTests { + + private DefaultPropertyMapper mapper = new DefaultPropertyMapper(); + + @Override + protected PropertyMapper getMapper() { + return this.mapper; + } + + @Test + public void mapFromStringShouldReturnBestGuess() throws Exception { + assertThat(namesFromString("server")).containsExactly("server"); + assertThat(namesFromString("server.port")).containsExactly("server.port"); + assertThat(namesFromString("host[0]")).containsExactly("host[0]"); + assertThat(namesFromString("host[0][1]")).containsExactly("host[0][1]"); + assertThat(namesFromString("host[0].name")).containsExactly("host[0].name"); + assertThat(namesFromString("host.f00.name")).containsExactly("host.f00.name"); + assertThat(namesFromString("my.host-name")).containsExactly("my.host-name"); + assertThat(namesFromString("my.hostName")).containsExactly("my.hostname"); + assertThat(namesFromString("my.HOST_NAME")).containsExactly("my.hostname"); + assertThat(namesFromString("s[!@#$%^&*()=+]e-rVeR")) + .containsExactly("s[!@#$%^&*()=+].e-rver"); + assertThat(namesFromString("host[FOO].name")).containsExactly("host[FOO].name"); + } + + @Test + public void mapFromConfigurationShouldReturnBestGuess() throws Exception { + assertThat(namesFromConfiguration("server")).containsExactly("server"); + assertThat(namesFromConfiguration("server.port")).containsExactly("server.port"); + assertThat(namesFromConfiguration("host[0]")).containsExactly("host[0]"); + assertThat(namesFromConfiguration("host[0][1]")).containsExactly("host[0][1]"); + assertThat(namesFromConfiguration("host[0].name")) + .containsExactly("host[0].name"); + assertThat(namesFromConfiguration("host.f00.name")) + .containsExactly("host.f00.name"); + assertThat(namesFromConfiguration("my.host-name")) + .containsExactly("my.host-name"); + assertThat(namesFromConfiguration("host[FOO].name")) + .containsExactly("host[FOO].name"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSourceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSourceTests.java new file mode 100644 index 0000000000..b0b760dc49 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/FilteredConfigurationPropertiesSourceTests.java @@ -0,0 +1,89 @@ +/* + * 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.Objects; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link FilteredConfigurationPropertiesSource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class FilteredConfigurationPropertiesSourceTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenSourceIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Source must not be null"); + new FilteredConfigurationPropertiesSource(null, Objects::nonNull); + } + + @Test + public void createWhenFilterIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Filter must not be null"); + new FilteredConfigurationPropertiesSource(new MockConfigurationPropertySource(), + null); + } + + @Test + public void iteratorShouldFilterNames() throws Exception { + MockConfigurationPropertySource source = createTestSource(); + ConfigurationPropertySource filtered = source.filter(this::noBrackets); + assertThat(filtered.iterator()).extracting(ConfigurationPropertyName::toString) + .containsExactly("a", "b", "c"); + } + + @Test + public void getValueShouldFilterNames() throws Exception { + MockConfigurationPropertySource source = createTestSource(); + ConfigurationPropertySource filtered = source.filter(this::noBrackets); + ConfigurationPropertyName name = ConfigurationPropertyName.of("a"); + assertThat(source.getConfigurationProperty(name).getValue()).isEqualTo("1"); + assertThat(filtered.getConfigurationProperty(name).getValue()).isEqualTo("1"); + ConfigurationPropertyName bracketName = ConfigurationPropertyName.of("a[1]"); + assertThat(source.getConfigurationProperty(bracketName).getValue()) + .isEqualTo("2"); + assertThat(filtered.getConfigurationProperty(bracketName)).isNull(); + + } + + private MockConfigurationPropertySource createTestSource() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("a", "1"); + source.put("a[1]", "2"); + source.put("b", "3"); + source.put("b[1]", "4"); + source.put("c", "5"); + return source; + } + + private boolean noBrackets(ConfigurationPropertyName name) { + return name.toString().indexOf("[") == -1; + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySourceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySourceTests.java new file mode 100644 index 0000000000..b5552dac41 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySourceTests.java @@ -0,0 +1,118 @@ +/* + * 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.LinkedHashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MapConfigurationPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class MapConfigurationPropertySourceTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenMapIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Map must not be null"); + new MapConfigurationPropertySource(null); + } + + @Test + public void createWhenMapHasEntriesShouldAdaptMap() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("foo.BAR", "spring"); + map.put(ConfigurationPropertyName.of("foo.baz"), "boot"); + MapConfigurationPropertySource source = new MapConfigurationPropertySource(map); + assertThat(getValue(source, "foo.bar")).isEqualTo("spring"); + assertThat(getValue(source, "foo.baz")).isEqualTo("boot"); + } + + @Test + public void putAllWhenMapIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Map must not be null"); + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.putAll(null); + } + + @Test + public void putAllShouldPutEntries() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("foo.BAR", "spring"); + map.put("foo.baz", "boot"); + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.putAll(map); + assertThat(getValue(source, "foo.bar")).isEqualTo("spring"); + assertThat(getValue(source, "foo.baz")).isEqualTo("boot"); + } + + @Test + public void putShouldPutEntry() throws Exception { + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.put("foo.bar", "baz"); + assertThat(getValue(source, "foo.bar")).isEqualTo("baz"); + } + + @Test + public void getConfigurationPropertyShouldGetFromMemory() throws Exception { + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.put("foo.bar", "baz"); + assertThat(getValue(source, "foo.bar")).isEqualTo("baz"); + source.put("foo.bar", "big"); + assertThat(getValue(source, "foo.bar")).isEqualTo("big"); + } + + @Test + public void iteratorShouldGetFromMemory() throws Exception { + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.put("foo.BAR", "spring"); + source.put("foo.baz", "boot"); + assertThat(source.iterator()).containsExactly( + ConfigurationPropertyName.of("foo.bar"), + ConfigurationPropertyName.of("foo.baz")); + } + + @Test + public void streamShouldGetFromMemory() throws Exception { + MapConfigurationPropertySource source = new MapConfigurationPropertySource(); + source.put("foo.BAR", "spring"); + source.put("foo.baz", "boot"); + assertThat(source.stream()).containsExactly( + ConfigurationPropertyName.of("foo.bar"), + ConfigurationPropertyName.of("foo.baz")); + + } + + private Object getValue(ConfigurationPropertySource source, String name) { + ConfigurationProperty property = source + .getConfigurationProperty(ConfigurationPropertyName.of(name)); + return (property == null ? null : property.getValue()); + }; + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java new file mode 100644 index 0000000000..94e01c9e72 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java @@ -0,0 +1,100 @@ +/* + * 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.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.boot.origin.MockOrigin; +import org.springframework.boot.origin.OriginTrackedValue; + +/** + * Mock {@link ConfigurationPropertySource} implementation used for testing. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class MockConfigurationPropertySource implements ConfigurationPropertySource { + + private final Map map = new LinkedHashMap<>(); + + private boolean nonIterable; + + public MockConfigurationPropertySource() { + } + + public MockConfigurationPropertySource(String configurationPropertyName, + Object value) { + this(configurationPropertyName, value, null); + } + + public MockConfigurationPropertySource(String configurationPropertyName, Object value, + String origin) { + put(ConfigurationPropertyName.of(configurationPropertyName), + OriginTrackedValue.of(value, MockOrigin.of(origin))); + } + + public void put(String name, String value) { + put(ConfigurationPropertyName.of(name), value); + } + + public void put(ConfigurationPropertyName name, String value) { + put(name, OriginTrackedValue.of(value)); + } + + private void put(ConfigurationPropertyName name, OriginTrackedValue value) { + this.map.put(name, value); + } + + public void setNonIterable(boolean nonIterable) { + this.nonIterable = nonIterable; + } + + @Override + public Iterator iterator() { + if (this.nonIterable) { + return Collections.emptyList().iterator(); + } + return this.map.keySet().iterator(); + } + + @Override + public Stream stream() { + if (this.nonIterable) { + return Collections.emptyList().stream(); + } + return this.map.keySet().stream(); + } + + @Override + public ConfigurationProperty getConfigurationProperty( + ConfigurationPropertyName name) { + OriginTrackedValue result = this.map.get(name); + if (result == null) { + result = findValue(name); + } + return ConfigurationProperty.of(name, result); + } + + private OriginTrackedValue findValue(ConfigurationPropertyName name) { + return this.map.get(name); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySourceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySourceTests.java new file mode 100644 index 0000000000..13a43baa37 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/PropertySourceConfigurationPropertySourceTests.java @@ -0,0 +1,265 @@ +/* + * 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.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PropertySourceConfigurationPropertySource}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class PropertySourceConfigurationPropertySourceTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenPropertySourceIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("PropertySource must not be null"); + new PropertySourceConfigurationPropertySource(null, mock(PropertyMapper.class)); + } + + @Test + public void createWhenMapperIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Mapper must not be null"); + new PropertySourceConfigurationPropertySource(mock(PropertySource.class), null); + } + + @Test + public void iteratorWhenNonEnumerbleShouldReturnEmptyIterator() throws Exception { + Map source = new LinkedHashMap<>(); + PropertySource propertySource = new NonEnumerablePropertySource<>( + new MapPropertySource("test", source)); + TestPropertyMapper mapper = new TestPropertyMapper(); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.iterator()).isEmpty(); + } + + @Test + public void iteratorShouldAdaptNames() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key1", "value1"); + source.put("key2", "value2"); + source.put("key3", "value3"); + source.put("key4", "value4"); + PropertySource propertySource = new MapPropertySource("test", source); + TestPropertyMapper mapper = new TestPropertyMapper(); + mapper.addFromProperySource("key1", "my.key1"); + mapper.addFromProperySource("key2", "my.key2a", "my.key2b"); + mapper.addFromProperySource("key4", "my.key4"); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.iterator()).extracting(Object::toString) + .containsExactly("my.key1", "my.key2a", "my.key2b", "my.key4"); + } + + @Test + public void getValueShouldUseDirectMapping() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key1", "value1"); + source.put("key2", "value2"); + source.put("key3", "value3"); + PropertySource propertySource = new NonEnumerablePropertySource<>( + new MapPropertySource("test", source)); + TestPropertyMapper mapper = new TestPropertyMapper(); + ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); + mapper.addFromConfigurationProperty(name, "key2"); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("value2"); + } + + @Test + public void getValueShouldFallbackToEnumerableMapping() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key1", "value1"); + source.put("key2", "value2"); + source.put("key3", "value3"); + PropertySource propertySource = new MapPropertySource("test", source); + TestPropertyMapper mapper = new TestPropertyMapper(); + mapper.addFromProperySource("key1", "my.missing"); + mapper.addFromProperySource("key2", "my.k-e-y"); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); + assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("value2"); + } + + @Test + public void getValueShouldUseExtractor() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key", "value"); + PropertySource propertySource = new NonEnumerablePropertySource<>( + new MapPropertySource("test", source)); + TestPropertyMapper mapper = new TestPropertyMapper(); + ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); + mapper.addFromConfigurationProperty(name, "key", + (value) -> value.toString().replace("ue", "let")); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("vallet"); + } + + @Test + public void getValueOrigin() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key", "value"); + PropertySource propertySource = new MapPropertySource("test", source); + TestPropertyMapper mapper = new TestPropertyMapper(); + ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); + mapper.addFromConfigurationProperty(name, "key"); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.getConfigurationProperty(name).getOrigin().toString()) + .isEqualTo("\"key\" from property source \"test\""); + } + + @Test + public void getValueWhenOriginCapableShouldIncludeSourceOrigin() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("key", "value"); + PropertySource propertySource = new OriginCapablePropertySource<>( + new MapPropertySource("test", source)); + TestPropertyMapper mapper = new TestPropertyMapper(); + ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); + mapper.addFromConfigurationProperty(name, "key"); + PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource( + propertySource, mapper); + assertThat(adapter.getConfigurationProperty(name).getOrigin().toString()) + .isEqualTo("TestOrigin key"); + } + + /** + * Test {@link PropertySource} that doesn't extend {@link EnumerablePropertySource}. + */ + private static class NonEnumerablePropertySource extends PropertySource { + + private final PropertySource propertySource; + + NonEnumerablePropertySource(PropertySource propertySource) { + super(propertySource.getName(), propertySource.getSource()); + this.propertySource = propertySource; + } + + @Override + public Object getProperty(String name) { + return this.propertySource.getProperty(name); + } + + } + + /** + * Test {@link PropertySource} that's also a {@link OriginLookup}. + */ + private static class OriginCapablePropertySource extends PropertySource + implements OriginLookup { + + private final PropertySource propertySource; + + OriginCapablePropertySource(PropertySource propertySource) { + super(propertySource.getName(), propertySource.getSource()); + this.propertySource = propertySource; + } + + @Override + public Object getProperty(String name) { + return this.propertySource.getProperty(name); + } + + @Override + public Origin getOrigin(String name) { + return new Origin() { + + @Override + public String toString() { + return "TestOrigin " + name; + } + + }; + } + + } + + /** + * Test {@link PropertyMapper} implementation. + */ + private static class TestPropertyMapper implements PropertyMapper { + + private MultiValueMap fromSource = new LinkedMultiValueMap<>(); + + private MultiValueMap fromConfig = new LinkedMultiValueMap<>(); + + public void addFromProperySource(String from, String... to) { + for (String configurationPropertyName : to) { + this.fromSource.add(from, new PropertyMapping(from, + ConfigurationPropertyName.of(configurationPropertyName))); + } + } + + public void addFromConfigurationProperty(ConfigurationPropertyName from, + String... to) { + for (String propertySourceName : to) { + this.fromConfig.add(from, new PropertyMapping(propertySourceName, from)); + } + } + + public void addFromConfigurationProperty(ConfigurationPropertyName from, + String to, Function extractor) { + this.fromConfig.add(from, new PropertyMapping(to, from, extractor)); + } + + @Override + public List map(PropertySource propertySource, + String propertySourceName) { + return this.fromSource.getOrDefault(propertySourceName, + Collections.emptyList()); + } + + @Override + public List map(PropertySource propertySource, + ConfigurationPropertyName configurationPropertyName) { + return this.fromConfig.getOrDefault(configurationPropertyName, + Collections.emptyList()); + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java new file mode 100644 index 0000000000..7033b06548 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java @@ -0,0 +1,131 @@ +/* + * 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.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SystemEnvironmentPropertyMapper}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +public class SystemEnvironmentPropertyMapperTests extends AbstractPropertyMapperTests { + + private SystemEnvironmentPropertyMapper mapper = new SystemEnvironmentPropertyMapper(); + + @Override + protected PropertyMapper getMapper() { + return this.mapper; + } + + @Test + public void mapFromStringShouldReturnBestGuess() throws Exception { + assertThat(namesFromString("SERVER")).containsExactly("server"); + assertThat(namesFromString("SERVER_PORT")).containsExactly("server.port"); + assertThat(namesFromString("HOST_0")).containsExactly("host[0]"); + assertThat(namesFromString("HOST_0_1")).containsExactly("host[0][1]"); + assertThat(namesFromString("HOST_0_NAME")).containsExactly("host[0].name"); + assertThat(namesFromString("HOST_F00_NAME")).containsExactly("host.f00.name"); + assertThat(namesFromString("S-ERVER")).containsExactly("s-erver"); + assertThat(namesFromString("SERVERS__", "1,2,3")).containsExactly("servers[0]", + "servers[1]", "servers[2]"); + assertThat(namesFromString("SERVERS_0__", "1,2,3")) + .containsExactly("servers[0][0]", "servers[0][1]", "servers[0][2]"); + } + + @Test + public void mapFromConfigurationShouldReturnBestGuess() throws Exception { + assertThat(namesFromConfiguration("server")).containsExactly("SERVER"); + assertThat(namesFromConfiguration("server.port")).containsExactly("SERVER_PORT"); + assertThat(namesFromConfiguration("host[0]")).containsExactly("HOST_0"); + assertThat(namesFromConfiguration("host[0][1]")).containsExactly("HOST_0_1"); + assertThat(namesFromConfiguration("host[0].name")).containsExactly("HOST_0_NAME"); + assertThat(namesFromConfiguration("host.f00.name")) + .containsExactly("HOST_F00_NAME"); + assertThat(namesFromConfiguration("foo.the-bar")).containsExactly("FOO_THEBAR"); + } + + @Test + public void mapFromStringWhenListShortcutShouldExtractValues() throws Exception { + Map source = new LinkedHashMap<>(); + source.put("SERVER__", "foo,bar,baz"); + PropertySource propertySource = new MapPropertySource("test", source); + List mappings = this.mapper.map(propertySource, "SERVER__"); + List result = new ArrayList<>(); + for (PropertyMapping mapping : mappings) { + Object value = propertySource.getProperty(mapping.getPropertySourceName()); + value = mapping.getValueExtractor().apply(value); + result.add(value); + } + assertThat(result).containsExactly("foo", "bar", "baz"); + } + + @Test + public void mapFromConfigurationShouldIncludeShortcutAndExtractValues() + throws Exception { + Map source = new LinkedHashMap<>(); + source.put("SERVER__", "foo,bar,baz"); + PropertySource propertySource = new MapPropertySource("test", source); + List mappings = this.mapper.map(propertySource, + ConfigurationPropertyName.of("server[1]")); + List result = new ArrayList<>(); + for (PropertyMapping mapping : mappings) { + Object value = propertySource.getProperty(mapping.getPropertySourceName()); + value = mapping.getValueExtractor().apply(value); + if (value != null) { + result.add(value); + } + } + assertThat(result).containsExactly("bar"); + } + + @Test + public void underscoreShouldNotMapToEmptyString() throws Exception { + Map source = new LinkedHashMap<>(); + PropertySource propertySource = new MapPropertySource("test", source); + List mappings = this.mapper.map(propertySource, "_"); + boolean applicable = false; + for (PropertyMapping mapping : mappings) { + applicable = mapping.isApplicable(ConfigurationPropertyName.of("")); + } + assertThat(applicable).isFalse(); + } + + @Test + public void underscoreWithWhitespaceShouldNotMapToEmptyString() throws Exception { + Map source = new LinkedHashMap<>(); + PropertySource propertySource = new MapPropertySource("test", source); + List mappings = this.mapper.map(propertySource, " _"); + boolean applicable = false; + for (PropertyMapping mapping : mappings) { + applicable = mapping.isApplicable(ConfigurationPropertyName.of("")); + } + assertThat(applicable).isFalse(); + } + +}