diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/CachingConfigurationPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/CachingConfigurationPropertySource.java new file mode 100644 index 00000000000..61b894d8159 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/CachingConfigurationPropertySource.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 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 + * + * https://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; + +/** + * Interface used to indicate that a {@link ConfigurationPropertySource} supports + * {@link ConfigurationPropertyCaching}. + * + * @author Phillip Webb + */ +interface CachingConfigurationPropertySource { + + /** + * Return {@link ConfigurationPropertyCaching} for this source. + * @return source caching + */ + ConfigurationPropertyCaching getCaching(); + + /** + * Find {@link ConfigurationPropertyCaching} for the given source. + * @param source the configuration property source + * @return a {@link ConfigurationPropertyCaching} instance or {@code null} if the + * source does not support caching. + */ + static ConfigurationPropertyCaching find(ConfigurationPropertySource source) { + if (source instanceof CachingConfigurationPropertySource) { + return ((CachingConfigurationPropertySource) source).getCaching(); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyCaching.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyCaching.java new file mode 100644 index 00000000000..4471b9dea47 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyCaching.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.time.Duration; + +import org.springframework.core.env.Environment; +import org.springframework.util.Assert; + +/** + * Interface that can be used to control configuration property source caches. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface ConfigurationPropertyCaching { + + /** + * Enable caching with an unlimited time-to-live. + */ + void enable(); + + /** + * Disable caching. + */ + void disable(); + + /** + * Set amount of time that an item can live in the cache. Calling this method will + * also enable the cache. + * @param timeToLive the time to live value. + */ + void setTimeToLive(Duration timeToLive); + + /** + * Clear the cache and force it to be reloaded on next access. + */ + void clear(); + + /** + * Get for all configuration property sources in the environment. + * @param environment the spring environment + * @return a caching instance that controls all sources in the environment + */ + static ConfigurationPropertyCaching get(Environment environment) { + return get(environment, null); + } + + /** + * Get for a specific configuration property source in the environment. + * @param environment the spring environment + * @param underlyingSource the + * {@link ConfigurationPropertySource#getUnderlyingSource() underlying source} that + * must match + * @return a caching instance that controls the matching source + */ + static ConfigurationPropertyCaching get(Environment environment, Object underlyingSource) { + Iterable sources = ConfigurationPropertySources.get(environment); + return get(sources, underlyingSource); + } + + /** + * Get for all specified configuration property sources. + * @param sources the configuration property sources + * @return a caching instance that controls the sources + */ + static ConfigurationPropertyCaching get(Iterable sources) { + return get(sources, null); + } + + /** + * Get for a specific configuration property source in the specified configuration + * property sources. + * @param sources the configuration property sources + * @param underlyingSource the + * {@link ConfigurationPropertySource#getUnderlyingSource() underlying source} that + * must match + * @return a caching instance that controls the matching source + */ + static ConfigurationPropertyCaching get(Iterable sources, Object underlyingSource) { + Assert.notNull(sources, "Sources must not be null"); + if (underlyingSource == null) { + return new ConfigurationPropertySourcesCaching(sources); + } + for (ConfigurationPropertySource source : sources) { + if (source.getUnderlyingSource() == underlyingSource) { + ConfigurationPropertyCaching caching = CachingConfigurationPropertySource.find(source); + if (caching != null) { + return caching; + } + } + } + throw new IllegalStateException("Unable to find cache from configuration property sources"); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesCaching.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesCaching.java new file mode 100644 index 00000000000..60df84baecf --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesCaching.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.time.Duration; +import java.util.function.Consumer; + +/** + * {@link ConfigurationPropertyCaching} for an {@link Iterable iterable} set of + * {@link ConfigurationPropertySource} instances. + * + * @author Phillip Webb + */ +class ConfigurationPropertySourcesCaching implements ConfigurationPropertyCaching { + + private final Iterable sources; + + ConfigurationPropertySourcesCaching(Iterable sources) { + this.sources = sources; + } + + @Override + public void enable() { + forEach(ConfigurationPropertyCaching::enable); + } + + @Override + public void disable() { + forEach(ConfigurationPropertyCaching::disable); + } + + @Override + public void setTimeToLive(Duration timeToLive) { + forEach((caching) -> caching.setTimeToLive(timeToLive)); + } + + @Override + public void clear() { + forEach(ConfigurationPropertyCaching::clear); + } + + private void forEach(Consumer action) { + if (this.sources != null) { + for (ConfigurationPropertySource source : this.sources) { + ConfigurationPropertyCaching caching = CachingConfigurationPropertySource.find(source); + if (caching != null) { + action.accept(caching); + } + } + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java index 5c36ff0e9f3..0aef2608886 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java @@ -16,6 +16,9 @@ package org.springframework.boot.context.properties.source; +import java.util.Collections; +import java.util.List; + import org.springframework.util.ObjectUtils; /** @@ -32,48 +35,48 @@ final class DefaultPropertyMapper implements PropertyMapper { public static final PropertyMapper INSTANCE = new DefaultPropertyMapper(); - private LastMapping lastMappedConfigurationPropertyName; + private LastMapping> lastMappedConfigurationPropertyName; - private LastMapping lastMappedPropertyName; + private LastMapping lastMappedPropertyName; private DefaultPropertyMapper() { } @Override - public PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName) { + public List map(ConfigurationPropertyName configurationPropertyName) { // Use a local copy in case another thread changes things - LastMapping last = this.lastMappedConfigurationPropertyName; + LastMapping> last = this.lastMappedConfigurationPropertyName; if (last != null && last.isFrom(configurationPropertyName)) { return last.getMapping(); } String convertedName = configurationPropertyName.toString(); - PropertyMapping[] mapping = { new PropertyMapping(convertedName, configurationPropertyName) }; + List mapping = Collections.singletonList(convertedName); this.lastMappedConfigurationPropertyName = new LastMapping<>(configurationPropertyName, mapping); return mapping; } @Override - public PropertyMapping[] map(String propertySourceName) { + public ConfigurationPropertyName map(String propertySourceName) { // Use a local copy in case another thread changes things - LastMapping last = this.lastMappedPropertyName; + LastMapping last = this.lastMappedPropertyName; if (last != null && last.isFrom(propertySourceName)) { return last.getMapping(); } - PropertyMapping[] mapping = tryMap(propertySourceName); + ConfigurationPropertyName mapping = tryMap(propertySourceName); this.lastMappedPropertyName = new LastMapping<>(propertySourceName, mapping); return mapping; } - private PropertyMapping[] tryMap(String propertySourceName) { + private ConfigurationPropertyName tryMap(String propertySourceName) { try { ConfigurationPropertyName convertedName = ConfigurationPropertyName.adapt(propertySourceName, '.'); if (!convertedName.isEmpty()) { - return new PropertyMapping[] { new PropertyMapping(propertySourceName, convertedName) }; + return convertedName; } } catch (Exception ex) { } - return NO_MAPPINGS; + return ConfigurationPropertyName.EMPTY; } @Override @@ -81,13 +84,13 @@ final class DefaultPropertyMapper implements PropertyMapper { return name.isAncestorOf(candidate); } - private static class LastMapping { + private static class LastMapping { private final T from; - private final PropertyMapping[] mapping; + private final M mapping; - LastMapping(T from, PropertyMapping[] mapping) { + LastMapping(T from, M mapping) { this.from = from; this.mapping = mapping; } @@ -96,7 +99,7 @@ final class DefaultPropertyMapper implements PropertyMapper { return ObjectUtils.nullSafeEquals(from, this.from); } - PropertyMapping[] getMapping() { + M getMapping() { return this.mapping; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java index 3bbc72e10a2..b24f13fa94f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MapConfigurationPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -35,6 +35,8 @@ import org.springframework.util.Assert; */ public class MapConfigurationPropertySource implements IterableConfigurationPropertySource { + private static final PropertyMapper[] DEFAULT_MAPPERS = { DefaultPropertyMapper.INSTANCE }; + private final Map source; private final IterableConfigurationPropertySource delegate; @@ -53,8 +55,8 @@ public class MapConfigurationPropertySource implements IterableConfigurationProp */ public MapConfigurationPropertySource(Map map) { this.source = new LinkedHashMap<>(); - this.delegate = new SpringIterableConfigurationPropertySource(new MapPropertySource("source", this.source), - DefaultPropertyMapper.INSTANCE); + MapPropertySource mapPropertySource = new MapPropertySource("source", this.source); + this.delegate = new SpringIterableConfigurationPropertySource(mapPropertySource, DEFAULT_MAPPERS); putAll(map); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java index 8fbe234333e..8840a368d12 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java @@ -16,6 +16,8 @@ package org.springframework.boot.context.properties.source; +import java.util.List; + import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.PropertySource; @@ -28,8 +30,7 @@ import org.springframework.core.env.PropertySource; * {@link SpringConfigurationPropertySource} to first attempt any direct mappings (i.e. * map the {@link ConfigurationPropertyName} directly to the {@link PropertySource} name) * before falling back to {@link EnumerablePropertySource enumerating} property names, - * mapping them to a {@link ConfigurationPropertyName} and checking for - * {@link PropertyMapping#isApplicable(ConfigurationPropertyName) applicability}. See + * mapping them to a {@link ConfigurationPropertyName} and checking for applicability. See * {@link SpringConfigurationPropertySource} for more details. * * @author Phillip Webb @@ -38,22 +39,21 @@ import org.springframework.core.env.PropertySource; */ interface PropertyMapper { - PropertyMapping[] NO_MAPPINGS = {}; - /** * Provide mappings from a {@link ConfigurationPropertySource} * {@link ConfigurationPropertyName}. * @param configurationPropertyName the name to map - * @return a stream of mappings or {@code Stream#empty()} + * @return the mapped names or an empty list */ - PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName); + List map(ConfigurationPropertyName configurationPropertyName); /** * Provide mappings from a {@link PropertySource} property name. * @param propertySourceName the name to map - * @return a stream of mappings or {@code Stream#empty()} + * @return the mapped configuration property name or + * {@link ConfigurationPropertyName#EMPTY} */ - PropertyMapping[] map(String propertySourceName); + ConfigurationPropertyName map(String propertySourceName); /** * Returns {@code true} if {@code name} is an ancestor (immediate or nested parent) of diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapping.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapping.java deleted file mode 100644 index ee4eaa86305..00000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapping.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2012-2019 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 - * - * https://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.core.env.PropertySource; - -/** - * Details a mapping between a {@link PropertySource} item and a - * {@link ConfigurationPropertySource} item. - * - * @author Phillip Webb - * @author Madhura Bhave - * @see SpringConfigurationPropertySource - */ -class PropertyMapping { - - private final String propertySourceName; - - private final ConfigurationPropertyName configurationPropertyName; - - /** - * 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 = propertySourceName; - this.configurationPropertyName = configurationPropertyName; - } - - /** - * Return the mapped {@link PropertySource} name. - * @return the property source name (never {@code null}) - */ - String getPropertySourceName() { - return this.propertySourceName; - } - - /** - * Return the mapped {@link ConfigurationPropertySource} - * {@link ConfigurationPropertyName}. - * @return the configuration property source name (never {@code null}) - */ - ConfigurationPropertyName getConfigurationPropertyName() { - return this.configurationPropertyName; - } - - /** - * Return if this mapping is applicable for the given - * {@link ConfigurationPropertyName}. - * @param name the name to check - * @return if the mapping is applicable - */ - boolean isApplicable(ConfigurationPropertyName name) { - return this.configurationPropertyName.equals(name); - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCache.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCache.java new file mode 100644 index 00000000000..c1cf95c2672 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCache.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.lang.ref.SoftReference; +import java.time.Duration; +import java.time.Instant; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +/** + * Simple cache that uses a {@link SoftReference} to cache a value for as long as + * possible. + * + * @param the value type + * @author Phillip Webb + */ +class SoftReferenceConfigurationPropertyCache implements ConfigurationPropertyCaching { + + private static final Duration UNLIMITED = Duration.ZERO; + + private final boolean neverExpire; + + private volatile Duration timeToLive; + + private volatile SoftReference value = new SoftReference<>(null); + + private volatile Instant lastAccessed = now(); + + SoftReferenceConfigurationPropertyCache(boolean neverExpire) { + this.neverExpire = neverExpire; + } + + @Override + public void enable() { + this.timeToLive = UNLIMITED; + } + + @Override + public void disable() { + this.timeToLive = null; + } + + @Override + public void setTimeToLive(Duration timeToLive) { + this.timeToLive = (timeToLive == null || timeToLive.isZero()) ? null : timeToLive; + } + + @Override + public void clear() { + this.lastAccessed = null; + } + + /** + * Get an value from the cache, creating it if necessary. + * @param factory a factory used to create the item if there is no reference to it. + * @param refreshAction action called to refresh the value if it has expired + * @return the value from the cache + */ + T get(Supplier factory, UnaryOperator refreshAction) { + T value = getValue(); + if (value == null) { + value = refreshAction.apply(factory.get()); + setValue(value); + } + else if (hasExpired()) { + value = refreshAction.apply(value); + setValue(value); + } + if (!this.neverExpire) { + this.lastAccessed = now(); + } + return value; + } + + private boolean hasExpired() { + if (this.neverExpire) { + return false; + } + Duration timeToLive = this.timeToLive; + Instant lastAccessed = this.lastAccessed; + if (timeToLive == null || lastAccessed == null) { + return true; + } + return !UNLIMITED.equals(timeToLive) && now().isAfter(lastAccessed.plus(timeToLive)); + } + + protected Instant now() { + return Instant.now(); + } + + protected T getValue() { + return this.value.get(); + } + + protected void setValue(T value) { + this.value = new SoftReference<>(value); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySource.java index 299d66724f5..b568464065e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySource.java @@ -18,7 +18,6 @@ package org.springframework.boot.context.properties.source; import java.util.Map; import java.util.Random; -import java.util.function.Function; import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.PropertySourceOrigin; @@ -27,7 +26,6 @@ import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.SystemEnvironmentPropertySource; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * {@link ConfigurationPropertySource} backed by a non-enumerable Spring @@ -55,38 +53,60 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource { private static final ConfigurationPropertyName RANDOM = ConfigurationPropertyName.of("random"); + private static final PropertyMapper[] DEFAULT_MAPPERS = { DefaultPropertyMapper.INSTANCE }; + + private static final PropertyMapper[] SYSTEM_ENVIRONMENT_MAPPERS = { SystemEnvironmentPropertyMapper.INSTANCE, + DefaultPropertyMapper.INSTANCE }; + private final PropertySource propertySource; - private final PropertyMapper mapper; - - private final Function containsDescendantOf; + private final PropertyMapper[] mappers; /** * Create a new {@link SpringConfigurationPropertySource} implementation. * @param propertySource the source property source - * @param mapper the property mapper - * @param containsDescendantOf function used to implement - * {@link #containsDescendantOf(ConfigurationPropertyName)} (may be {@code null}) + * @param mappers the property mappers */ - SpringConfigurationPropertySource(PropertySource propertySource, PropertyMapper mapper, - Function containsDescendantOf) { + SpringConfigurationPropertySource(PropertySource propertySource, PropertyMapper... mappers) { Assert.notNull(propertySource, "PropertySource must not be null"); - Assert.notNull(mapper, "Mapper must not be null"); this.propertySource = propertySource; - this.mapper = (mapper instanceof DelegatingPropertyMapper) ? mapper : new DelegatingPropertyMapper(mapper); - this.containsDescendantOf = (containsDescendantOf != null) ? containsDescendantOf - : (n) -> ConfigurationPropertyState.UNKNOWN; + this.mappers = mappers; } @Override public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) { - PropertyMapping[] mappings = getMapper().map(name); - return find(mappings, name); + if (name == null) { + return null; + } + for (PropertyMapper mapper : this.mappers) { + try { + for (String candidate : mapper.map(name)) { + Object value = getPropertySource().getProperty(candidate); + if (value != null) { + Origin origin = PropertySourceOrigin.get(getPropertySource(), candidate); + return ConfigurationProperty.of(name, value, origin); + } + } + } + catch (Exception ex) { + } + } + return null; } @Override public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { - return this.containsDescendantOf.apply(name); + if (getPropertySource().getSource() instanceof Random) { + return containsDescendantOfForRandom(name); + } + return ConfigurationPropertyState.UNKNOWN; + } + + private static ConfigurationPropertyState containsDescendantOfForRandom(ConfigurationPropertyName name) { + if (name.isAncestorOf(RANDOM) || name.equals(RANDOM)) { + return ConfigurationPropertyState.PRESENT; + } + return ConfigurationPropertyState.ABSENT; } @Override @@ -94,35 +114,12 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource { return this.propertySource; } - protected final ConfigurationProperty find(PropertyMapping[] mappings, ConfigurationPropertyName name) { - for (PropertyMapping candidate : mappings) { - if (candidate.isApplicable(name)) { - ConfigurationProperty result = find(candidate); - if (result != null) { - return result; - } - } - } - return null; - } - - private ConfigurationProperty find(PropertyMapping mapping) { - String propertySourceName = mapping.getPropertySourceName(); - Object value = getPropertySource().getProperty(propertySourceName); - if (value == null) { - return null; - } - ConfigurationPropertyName configurationPropertyName = mapping.getConfigurationPropertyName(); - Origin origin = PropertySourceOrigin.get(this.propertySource, propertySourceName); - return ConfigurationProperty.of(configurationPropertyName, value, origin); - } - protected PropertySource getPropertySource() { return this.propertySource; } - protected final PropertyMapper getMapper() { - return this.mapper; + protected final PropertyMapper[] getMappers() { + return this.mappers; } @Override @@ -139,19 +136,18 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource { */ static SpringConfigurationPropertySource from(PropertySource source) { Assert.notNull(source, "Source must not be null"); - PropertyMapper mapper = getPropertyMapper(source); + PropertyMapper[] mappers = getPropertyMappers(source); if (isFullEnumerable(source)) { - return new SpringIterableConfigurationPropertySource((EnumerablePropertySource) source, mapper); + return new SpringIterableConfigurationPropertySource((EnumerablePropertySource) source, mappers); } - return new SpringConfigurationPropertySource(source, mapper, getContainsDescendantOfForSource(source)); + return new SpringConfigurationPropertySource(source, mappers); } - private static PropertyMapper getPropertyMapper(PropertySource source) { + private static PropertyMapper[] getPropertyMappers(PropertySource source) { if (source instanceof SystemEnvironmentPropertySource && hasSystemEnvironmentName(source)) { - return new DelegatingPropertyMapper(SystemEnvironmentPropertyMapper.INSTANCE, - DefaultPropertyMapper.INSTANCE); + return SYSTEM_ENVIRONMENT_MAPPERS; } - return new DelegatingPropertyMapper(DefaultPropertyMapper.INSTANCE); + return DEFAULT_MAPPERS; } private static boolean hasSystemEnvironmentName(PropertySource source) { @@ -181,97 +177,4 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource { return source; } - private static Function getContainsDescendantOfForSource( - PropertySource source) { - if (source.getSource() instanceof Random) { - return SpringConfigurationPropertySource::containsDescendantOfForRandom; - } - return null; - } - - private static ConfigurationPropertyState containsDescendantOfForRandom(ConfigurationPropertyName name) { - if (name.isAncestorOf(RANDOM) || name.equals(RANDOM)) { - return ConfigurationPropertyState.PRESENT; - } - return ConfigurationPropertyState.ABSENT; - } - - /** - * {@link PropertyMapper} that delegates to other {@link PropertyMapper}s and also - * swallows exceptions when the mapping fails. - */ - private static class DelegatingPropertyMapper implements PropertyMapper { - - private static final PropertyMapping[] NONE = {}; - - private final PropertyMapper first; - - private final PropertyMapper second; - - DelegatingPropertyMapper(PropertyMapper first) { - this(first, null); - } - - DelegatingPropertyMapper(PropertyMapper first, PropertyMapper second) { - this.first = first; - this.second = second; - } - - @Override - public PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName) { - PropertyMapping[] first = map(this.first, configurationPropertyName); - PropertyMapping[] second = map(this.second, configurationPropertyName); - return merge(first, second); - } - - private PropertyMapping[] map(PropertyMapper mapper, ConfigurationPropertyName configurationPropertyName) { - try { - return (mapper != null) ? mapper.map(configurationPropertyName) : NONE; - } - catch (Exception ex) { - return NONE; - } - } - - @Override - public PropertyMapping[] map(String propertySourceName) { - PropertyMapping[] first = map(this.first, propertySourceName); - PropertyMapping[] second = map(this.second, propertySourceName); - return merge(first, second); - } - - private PropertyMapping[] map(PropertyMapper mapper, String propertySourceName) { - try { - return (mapper != null) ? mapper.map(propertySourceName) : NONE; - } - catch (Exception ex) { - return NONE; - } - } - - @Override - public boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) { - return isAncestorOf(this.first, name, candidate) || isAncestorOf(this.second, name, candidate); - } - - private boolean isAncestorOf(PropertyMapper mapper, ConfigurationPropertyName name, - ConfigurationPropertyName candidate) { - return mapper != null && mapper.isAncestorOf(name, candidate); - } - - private PropertyMapping[] merge(PropertyMapping[] first, PropertyMapping[] second) { - if (ObjectUtils.isEmpty(second)) { - return first; - } - if (ObjectUtils.isEmpty(first)) { - return second; - } - PropertyMapping[] merged = new PropertyMapping[first.length + second.length]; - System.arraycopy(first, 0, merged, 0, first.length); - System.arraycopy(second, 0, merged, first.length, second.length); - return merged; - } - - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySources.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySources.java index feb31215c42..09ccf172359 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySources.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySources.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java index 923a4997cb9..774f33aa6ca 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java @@ -17,21 +17,27 @@ package org.springframework.boot.context.properties.source; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.ConcurrentModificationException; -import java.util.HashSet; +import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Set; +import java.util.Map; +import java.util.function.Supplier; import java.util.stream.Stream; +import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.OriginLookup; +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.core.env.StandardEnvironment; import org.springframework.core.env.SystemEnvironmentPropertySource; -import org.springframework.util.ObjectUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * {@link ConfigurationPropertySource} backed by an {@link EnumerablePropertySource}. @@ -45,13 +51,16 @@ import org.springframework.util.ObjectUtils; * @see PropertyMapper */ class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySource - implements IterableConfigurationPropertySource { + implements IterableConfigurationPropertySource, CachingConfigurationPropertySource { - private volatile Cache cache; + private volatile Collection configurationPropertyNames; - SpringIterableConfigurationPropertySource(EnumerablePropertySource propertySource, PropertyMapper mapper) { - super(propertySource, mapper, null); + private final SoftReferenceConfigurationPropertyCache cache; + + SpringIterableConfigurationPropertySource(EnumerablePropertySource propertySource, PropertyMapper... mappers) { + super(propertySource, mappers); assertEnumerablePropertySource(); + this.cache = new SoftReferenceConfigurationPropertyCache<>(isImmutablePropertySource()); } private void assertEnumerablePropertySource() { @@ -65,13 +74,28 @@ class SpringIterableConfigurationPropertySource extends SpringConfigurationPrope } } + @Override + public ConfigurationPropertyCaching getCaching() { + return this.cache; + } + @Override public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) { - ConfigurationProperty configurationProperty = super.getConfigurationProperty(name); - if (configurationProperty == null) { - configurationProperty = find(getPropertyMappings(getCache()), name); + if (name == null) { + return null; } - return configurationProperty; + ConfigurationProperty configurationProperty = super.getConfigurationProperty(name); + if (configurationProperty != null) { + return configurationProperty; + } + for (String candidate : getMappings().getMapped(name)) { + Object value = getPropertySource().getProperty(candidate); + if (value != null) { + Origin origin = PropertySourceOrigin.get(getPropertySource(), candidate); + return ConfigurationProperty.of(name, value, origin); + } + } + return null; } @Override @@ -84,64 +108,59 @@ class SpringIterableConfigurationPropertySource extends SpringConfigurationPrope return getConfigurationPropertyNames().iterator(); } + private Collection getConfigurationPropertyNames() { + if (!isImmutablePropertySource()) { + return getMappings().getConfigurationPropertyNames(getPropertySource().getPropertyNames()); + } + Collection configurationPropertyNames = this.configurationPropertyNames; + if (configurationPropertyNames == null) { + configurationPropertyNames = getMappings() + .getConfigurationPropertyNames(getPropertySource().getPropertyNames()); + this.configurationPropertyNames = configurationPropertyNames; + } + return configurationPropertyNames; + } + @Override public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { - return ConfigurationPropertyState.search(this, (candidate) -> getMapper().isAncestorOf(name, candidate)); - } - - private List getConfigurationPropertyNames() { - Cache cache = getCache(); - List names = (cache != null) ? cache.getNames() : null; - if (names != null) { - return names; - } - PropertyMapping[] mappings = getPropertyMappings(cache); - names = new ArrayList<>(mappings.length); - for (PropertyMapping mapping : mappings) { - names.add(mapping.getConfigurationPropertyName()); - } - names = Collections.unmodifiableList(names); - if (cache != null) { - cache.setNames(names); - } - return names; - } - - private PropertyMapping[] getPropertyMappings(Cache cache) { - PropertyMapping[] result = (cache != null) ? cache.getMappings() : null; - if (result != null) { - return result; - } - String[] names = getPropertySource().getPropertyNames(); - List mappings = new ArrayList<>(names.length * 2); - for (String name : names) { - Collections.addAll(mappings, getMapper().map(name)); - } - result = mappings.toArray(new PropertyMapping[0]); - if (cache != null) { - cache.setMappings(result); + ConfigurationPropertyState result = super.containsDescendantOf(name); + if (result == ConfigurationPropertyState.UNKNOWN) { + result = ConfigurationPropertyState.search(this, (candidate) -> isAncestorOf(name, candidate)); } return result; } - private Cache getCache() { - CacheKey key = CacheKey.get(getPropertySource()); - if (key == null) { - return null; - } - Cache cache = this.cache; - try { - if (cache != null && cache.hasKeyEqualTo(key)) { - return cache; + private boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) { + for (PropertyMapper mapper : getMappers()) { + if (mapper.isAncestorOf(name, candidate)) { + return true; } - cache = new Cache(key.copy()); - this.cache = cache; - return cache; } - catch (ConcurrentModificationException ex) { - // Not fatal at this point, we can continue without a cache - return null; + return false; + } + + private Mappings getMappings() { + return this.cache.get(this::createMappings, this::updateMappings); + } + + private Mappings createMappings() { + return new Mappings(getMappers(), isImmutablePropertySource()); + } + + private Mappings updateMappings(Mappings mappings) { + mappings.updateMappings(getPropertySource()::getPropertyNames); + return mappings; + } + + private boolean isImmutablePropertySource() { + EnumerablePropertySource source = getPropertySource(); + if (source instanceof OriginLookup) { + return ((OriginLookup) source).isImmutable(); } + if (StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME.equals(source.getName())) { + return source.getSource() == System.getenv(); + } + return false; } @Override @@ -149,100 +168,92 @@ class SpringIterableConfigurationPropertySource extends SpringConfigurationPrope return (EnumerablePropertySource) super.getPropertySource(); } - private static class Cache { + private static class Mappings { - private final CacheKey key; + private final PropertyMapper[] mappers; - private List names; + private final boolean immutable; - private PropertyMapping[] mappings; + private volatile MultiValueMap mappings; - Cache(CacheKey key) { - this.key = key; + private volatile Map reverseMappings; + + private volatile Collection configurationPropertyNames; + + private volatile String[] lastUpdated; + + Mappings(PropertyMapper[] mappers, boolean immutable) { + this.mappers = mappers; + this.immutable = immutable; } - boolean hasKeyEqualTo(CacheKey key) { - return this.key.equals(key); + void updateMappings(Supplier propertyNames) { + if (this.mappings == null || !this.immutable) { + int count = 0; + while (true) { + try { + updateMappings(propertyNames.get()); + return; + } + catch (ConcurrentModificationException ex) { + if (count++ > 10) { + throw ex; + } + } + } + } } - List getNames() { - return this.names; - } - - void setNames(List names) { - this.names = names; - } - - PropertyMapping[] getMappings() { - return this.mappings; - } - - void setMappings(PropertyMapping[] mappings) { + private void updateMappings(String[] propertyNames) { + String[] lastUpdated = this.lastUpdated; + if (lastUpdated != null && Arrays.equals(lastUpdated, propertyNames)) { + return; + } + MultiValueMap previousMappings = this.mappings; + MultiValueMap mappings = (previousMappings != null) + ? new LinkedMultiValueMap<>(previousMappings) : new LinkedMultiValueMap<>(propertyNames.length); + Map previousReverseMappings = this.reverseMappings; + Map reverseMappings = (previousReverseMappings != null) + ? new HashMap<>(previousReverseMappings) : new HashMap<>(propertyNames.length); + for (PropertyMapper propertyMapper : this.mappers) { + for (String propertyName : propertyNames) { + if (!reverseMappings.containsKey(propertyName)) { + ConfigurationPropertyName configurationPropertyName = propertyMapper.map(propertyName); + if (configurationPropertyName != null && !configurationPropertyName.isEmpty()) { + mappings.add(configurationPropertyName, propertyName); + reverseMappings.put(propertyName, configurationPropertyName); + } + } + } + } this.mappings = mappings; + this.reverseMappings = reverseMappings; + this.lastUpdated = this.immutable ? null : propertyNames; + this.configurationPropertyNames = this.immutable + ? Collections.unmodifiableCollection(reverseMappings.values()) : null; } - } - - private static final class CacheKey { - - private static final CacheKey IMMUTABLE_PROPERTY_SOURCE = new CacheKey(new Object[0]); - - private final Object key; - - private CacheKey(Object key) { - this.key = key; + List getMapped(ConfigurationPropertyName configurationPropertyName) { + return this.mappings.getOrDefault(configurationPropertyName, Collections.emptyList()); } - CacheKey copy() { - if (this == IMMUTABLE_PROPERTY_SOURCE) { - return IMMUTABLE_PROPERTY_SOURCE; + Collection getConfigurationPropertyNames(String[] propertyNames) { + Collection names = this.configurationPropertyNames; + if (names != null) { + return names; } - return new CacheKey(copyKey(this.key)); - } - - private Object copyKey(Object key) { - if (key instanceof Set) { - return new HashSet((Set) key); + Map reverseMappings = this.reverseMappings; + if (reverseMappings == null || reverseMappings.isEmpty()) { + return Collections.emptySet(); } - return ((String[]) key).clone(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; + List relevantNames = new ArrayList<>(reverseMappings.size()); + for (String propertyName : propertyNames) { + ConfigurationPropertyName configurationPropertyName = reverseMappings.get(propertyName); + if (configurationPropertyName != null) { + relevantNames.add(configurationPropertyName); + } } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - CacheKey otherCacheKey = (CacheKey) obj; - return ObjectUtils.nullSafeEquals(this.key, otherCacheKey.key); - } - - @Override - public int hashCode() { - return this.key.hashCode(); - } - - static CacheKey get(EnumerablePropertySource source) { - if (isImmutable(source)) { - return IMMUTABLE_PROPERTY_SOURCE; - } - if (source instanceof MapPropertySource) { - MapPropertySource mapPropertySource = (MapPropertySource) source; - return new CacheKey(mapPropertySource.getSource().keySet()); - } - return new CacheKey(source.getPropertyNames()); - } - - private static boolean isImmutable(EnumerablePropertySource source) { - if (source instanceof OriginLookup) { - return ((OriginLookup) source).isImmutable(); - } - if (StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME.equals(source.getName())) { - return source.getSource() == System.getenv(); - } - return false; + return relevantNames; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java index 55504c9426b..7eb0cfe8e03 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java @@ -16,6 +16,9 @@ package org.springframework.boot.context.properties.source; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Locale; import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; @@ -37,59 +40,13 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper { public static final PropertyMapper INSTANCE = new SystemEnvironmentPropertyMapper(); @Override - public PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName) { + public List map(ConfigurationPropertyName configurationPropertyName) { String name = convertName(configurationPropertyName); String legacyName = convertLegacyName(configurationPropertyName); if (name.equals(legacyName)) { - return new PropertyMapping[] { new PropertyMapping(name, configurationPropertyName) }; - } - return new PropertyMapping[] { new PropertyMapping(name, configurationPropertyName), - new PropertyMapping(legacyName, configurationPropertyName) }; - } - - @Override - public PropertyMapping[] map(String propertySourceName) { - ConfigurationPropertyName name = convertName(propertySourceName); - if (name == null || name.isEmpty()) { - return NO_MAPPINGS; - } - return new PropertyMapping[] { new PropertyMapping(propertySourceName, name) }; - } - - @Override - public boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) { - return name.isAncestorOf(candidate) || isLegacyAncestorOf(name, candidate); - } - - private boolean isLegacyAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) { - if (!hasDashedEntries(name)) { - return false; - } - StringBuilder legacyCompatibleName = new StringBuilder(); - for (int i = 0; i < name.getNumberOfElements(); i++) { - legacyCompatibleName.append((i != 0) ? "." : ""); - legacyCompatibleName.append(name.getElement(i, Form.DASHED).replace('-', '.')); - } - return ConfigurationPropertyName.isValid(legacyCompatibleName) - && ConfigurationPropertyName.of(legacyCompatibleName).isAncestorOf(candidate); - } - - boolean hasDashedEntries(ConfigurationPropertyName name) { - for (int i = 0; i < name.getNumberOfElements(); i++) { - if (name.getElement(i, Form.DASHED).indexOf('-') != -1) { - return true; - } - } - return false; - } - - private ConfigurationPropertyName convertName(String propertySourceName) { - try { - return ConfigurationPropertyName.adapt(propertySourceName, '_', this::processElementValue); - } - catch (Exception ex) { - return null; + return Collections.singletonList(name); } + return Arrays.asList(name, legacyName); } private String convertName(ConfigurationPropertyName name) { @@ -122,6 +79,20 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper { return element.replace('-', '_').toUpperCase(Locale.ENGLISH); } + @Override + public ConfigurationPropertyName map(String propertySourceName) { + return convertName(propertySourceName); + } + + private ConfigurationPropertyName convertName(String propertySourceName) { + try { + return ConfigurationPropertyName.adapt(propertySourceName, '_', this::processElementValue); + } + catch (Exception ex) { + return ConfigurationPropertyName.EMPTY; + } + } + private CharSequence processElementValue(CharSequence value) { String result = value.toString().toLowerCase(Locale.ENGLISH); return isNumber(result) ? "[" + result + "]" : result; @@ -131,4 +102,31 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper { return string.chars().allMatch(Character::isDigit); } + @Override + public boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) { + return name.isAncestorOf(candidate) || isLegacyAncestorOf(name, candidate); + } + + private boolean isLegacyAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) { + if (!hasDashedEntries(name)) { + return false; + } + StringBuilder legacyCompatibleName = new StringBuilder(); + for (int i = 0; i < name.getNumberOfElements(); i++) { + legacyCompatibleName.append((i != 0) ? "." : ""); + legacyCompatibleName.append(name.getElement(i, Form.DASHED).replace('-', '.')); + } + return ConfigurationPropertyName.isValid(legacyCompatibleName) + && ConfigurationPropertyName.of(legacyCompatibleName).isAncestorOf(candidate); + } + + boolean hasDashedEntries(ConfigurationPropertyName name) { + for (int i = 0; i < name.getNumberOfElements(); i++) { + if (name.getElement(i, Form.DASHED).indexOf('-') != -1) { + return true; + } + } + return false; + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java index a66fb7eff85..a9460d71523 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/AbstractPropertyMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -16,8 +16,7 @@ package org.springframework.boot.context.properties.source; -import java.util.Arrays; -import java.util.Iterator; +import java.util.List; /** * Abstract base class for {@link PropertyMapper} tests. @@ -30,14 +29,12 @@ public abstract class AbstractPropertyMapperTests { protected abstract PropertyMapper getMapper(); - protected final Iterator namesFromString(String name) { - return Arrays.stream(getMapper().map(name)).map((mapping) -> mapping.getConfigurationPropertyName().toString()) - .iterator(); + protected final List mapConfigurationPropertyName(String configurationPropertyName) { + return getMapper().map(ConfigurationPropertyName.of(configurationPropertyName)); } - protected final Iterator namesFromConfiguration(String name) { - return Arrays.stream(getMapper().map(ConfigurationPropertyName.of(name))) - .map(PropertyMapping::getPropertySourceName).iterator(); + protected final String mapPropertySourceName(String propertySourceName) { + return getMapper().map(propertySourceName).toString(); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/CachingConfigurationPropertySourceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/CachingConfigurationPropertySourceTests.java new file mode 100644 index 00000000000..fee5d162939 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/CachingConfigurationPropertySourceTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link CachingConfigurationPropertySource}. + * + * @author Phillip Webb + */ +class CachingConfigurationPropertySourceTests { + + @Test + void findWhenNullSourceReturnsNull() { + ConfigurationPropertySource source = null; + assertThat(CachingConfigurationPropertySource.find(source)).isNull(); + } + + @Test + void findWhenNotCachingConfigurationPropertySourceReturnsNull() { + ConfigurationPropertySource source = mock(ConfigurationPropertySource.class); + assertThat(CachingConfigurationPropertySource.find(source)).isNull(); + } + + @Test + void findWhenCachingConfigurationPropertySourceReturnsCaching() { + ConfigurationPropertySource source = mock(ConfigurationPropertySource.class, + withSettings().extraInterfaces(CachingConfigurationPropertySource.class)); + ConfigurationPropertyCaching caching = mock(ConfigurationPropertyCaching.class); + given(((CachingConfigurationPropertySource) source).getCaching()).willReturn(caching); + assertThat(CachingConfigurationPropertySource.find(source)).isEqualTo(caching); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyCachingTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyCachingTests.java new file mode 100644 index 00000000000..13edbc49ed2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyCachingTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2020 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 + * + * https://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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ConfigurationPropertyCaching}. + * + * @author Phillip Webb + */ +class ConfigurationPropertyCachingTests { + + private StandardEnvironment environment; + + private MapPropertySource propertySource; + + @BeforeEach + void setup() { + this.environment = new StandardEnvironment(); + this.propertySource = new MapPropertySource("test", Collections.singletonMap("spring", "boot")); + this.environment.getPropertySources().addLast(this.propertySource); + } + + @Test + void getFromEnvironmentReturnsCaching() { + ConfigurationPropertyCaching caching = ConfigurationPropertyCaching.get(this.environment); + assertThat(caching).isInstanceOf(ConfigurationPropertySourcesCaching.class); + } + + @Test + void getFromEnvironmentForUnderlyingSourceReturnsCaching() { + ConfigurationPropertyCaching caching = ConfigurationPropertyCaching.get(this.environment, this.propertySource); + assertThat(caching).isInstanceOf(SoftReferenceConfigurationPropertyCache.class); + } + + @Test + void getFromSourcesWhenSourcesIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ConfigurationPropertyCaching.get((Iterable) null)) + .withMessage("Sources must not be null"); + } + + @Test + void getFromSourcesReturnsCachingComposite() { + List sources = new ArrayList<>(); + sources.add(SpringConfigurationPropertySource.from(this.propertySource)); + ConfigurationPropertyCaching caching = ConfigurationPropertyCaching.get(sources); + assertThat(caching).isInstanceOf(ConfigurationPropertySourcesCaching.class); + } + + @Test + void getFromSourcesForUnderlyingSourceReturnsCaching() { + List sources = new ArrayList<>(); + sources.add(SpringConfigurationPropertySource.from(this.propertySource)); + ConfigurationPropertyCaching caching = ConfigurationPropertyCaching.get(sources, this.propertySource); + assertThat(caching).isInstanceOf(SoftReferenceConfigurationPropertyCache.class); + } + + @Test + void getFromSourcesForUnderlyingSourceWhenCantFindThrowsException() { + List sources = new ArrayList<>(); + sources.add(SpringConfigurationPropertySource.from(this.propertySource)); + MapPropertySource anotherPropertySource = new MapPropertySource("test2", Collections.emptyMap()); + assertThatIllegalStateException() + .isThrownBy(() -> ConfigurationPropertyCaching.get(sources, anotherPropertySource)) + .withMessage("Unable to find cache from configuration property sources"); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesCachingTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesCachingTests.java new file mode 100644 index 00000000000..73aff6f7b4d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesCachingTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link ConfigurationPropertySourcesCaching}. + * + * @author Phillip Webb + */ +class ConfigurationPropertySourcesCachingTests { + + private List sources; + + private ConfigurationPropertySourcesCaching caching; + + @BeforeEach + void setup() { + this.sources = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + this.sources.add(mockSource(i % 2 == 0)); + } + this.caching = new ConfigurationPropertySourcesCaching(this.sources); + } + + private ConfigurationPropertySource mockSource(boolean cachingSource) { + if (!cachingSource) { + return mock(ConfigurationPropertySource.class); + } + ConfigurationPropertySource source = mock(ConfigurationPropertySource.class, + withSettings().extraInterfaces(CachingConfigurationPropertySource.class)); + ConfigurationPropertyCaching caching = mock(ConfigurationPropertyCaching.class); + given(((CachingConfigurationPropertySource) source).getCaching()).willReturn(caching); + return source; + } + + @Test + void enableDelegatesToCachingConfigurationPropertySources() { + this.caching.enable(); + verify(getCaching(0)).enable(); + verify(getCaching(2)).enable(); + } + + @Test + void enableWhenSourcesIsNullDoesNothing() { + new ConfigurationPropertySourcesCaching(null).enable(); + } + + @Test + void disableDelegatesToCachingConfigurationPropertySources() { + this.caching.disable(); + verify(getCaching(0)).disable(); + verify(getCaching(2)).disable(); + } + + @Test + void disableWhenSourcesIsNullDoesNothing() { + new ConfigurationPropertySourcesCaching(null).disable(); + } + + @Test + void setTimeToLiveDelegatesToCachingConfigurationPropertySources() { + Duration ttl = Duration.ofDays(1); + this.caching.setTimeToLive(ttl); + verify(getCaching(0)).setTimeToLive(ttl); + verify(getCaching(2)).setTimeToLive(ttl); + } + + @Test + void setTimeToLiveWhenSourcesIsNullDoesNothing() { + new ConfigurationPropertySourcesCaching(null).setTimeToLive(Duration.ofSeconds(1)); + } + + @Test + void clearDelegatesToCachingConfigurationPropertySources() { + this.caching.clear(); + verify(getCaching(0)).clear(); + verify(getCaching(2)).clear(); + } + + @Test + void clearWhenSourcesIsNullDoesNothing() { + new ConfigurationPropertySourcesCaching(null).enable(); + } + + private ConfigurationPropertyCaching getCaching(int index) { + return CachingConfigurationPropertySource.find(this.sources.get(index)); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java index f7e05e26303..8b2e913a4ca 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java @@ -130,6 +130,13 @@ class ConfigurationPropertySourcesTests { testPropertySourcePerformance(true, 1000); } + @Test // gh-20625 + void environmentPropertyAccessWhenMutableWithCacheShouldBePerformant() { + StandardEnvironment environment = createPerformanceTestEnvironment(false); + ConfigurationPropertyCaching.get(environment).enable(); + testPropertySourcePerformance(environment, 1000); + } + @Test // gh-20625 @Disabled("for manual testing") void environmentPropertyAccessWhenMutableShouldBeTolerable() { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java index 3ee430b5227..9bbf84e1b9e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/DefaultPropertyMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -35,29 +35,29 @@ class DefaultPropertyMapperTests extends AbstractPropertyMapperTests { @Test void mapFromStringShouldReturnBestGuess() { - assertThat(namesFromString("server")).toIterable().containsExactly("server"); - assertThat(namesFromString("server.port")).toIterable().containsExactly("server.port"); - assertThat(namesFromString("host[0]")).toIterable().containsExactly("host[0]"); - assertThat(namesFromString("host[0][1]")).toIterable().containsExactly("host[0][1]"); - assertThat(namesFromString("host[0].name")).toIterable().containsExactly("host[0].name"); - assertThat(namesFromString("host.f00.name")).toIterable().containsExactly("host.f00.name"); - assertThat(namesFromString("my.host-name")).toIterable().containsExactly("my.host-name"); - assertThat(namesFromString("my.hostName")).toIterable().containsExactly("my.hostname"); - assertThat(namesFromString("my.HOST_NAME")).toIterable().containsExactly("my.hostname"); - assertThat(namesFromString("s[!@#$%^&*()=+]e-rVeR")).toIterable().containsExactly("s[!@#$%^&*()=+].e-rver"); - assertThat(namesFromString("host[FOO].name")).toIterable().containsExactly("host[FOO].name"); + assertThat(mapPropertySourceName("server")).isEqualTo("server"); + assertThat(mapPropertySourceName("server.port")).isEqualTo("server.port"); + assertThat(mapPropertySourceName("host[0]")).isEqualTo("host[0]"); + assertThat(mapPropertySourceName("host[0][1]")).isEqualTo("host[0][1]"); + assertThat(mapPropertySourceName("host[0].name")).isEqualTo("host[0].name"); + assertThat(mapPropertySourceName("host.f00.name")).isEqualTo("host.f00.name"); + assertThat(mapPropertySourceName("my.host-name")).isEqualTo("my.host-name"); + assertThat(mapPropertySourceName("my.hostName")).isEqualTo("my.hostname"); + assertThat(mapPropertySourceName("my.HOST_NAME")).isEqualTo("my.hostname"); + assertThat(mapPropertySourceName("s[!@#$%^&*()=+]e-rVeR")).isEqualTo("s[!@#$%^&*()=+].e-rver"); + assertThat(mapPropertySourceName("host[FOO].name")).isEqualTo("host[FOO].name"); } @Test void mapFromConfigurationShouldReturnBestGuess() { - assertThat(namesFromConfiguration("server")).toIterable().containsExactly("server"); - assertThat(namesFromConfiguration("server.port")).toIterable().containsExactly("server.port"); - assertThat(namesFromConfiguration("host[0]")).toIterable().containsExactly("host[0]"); - assertThat(namesFromConfiguration("host[0][1]")).toIterable().containsExactly("host[0][1]"); - assertThat(namesFromConfiguration("host[0].name")).toIterable().containsExactly("host[0].name"); - assertThat(namesFromConfiguration("host.f00.name")).toIterable().containsExactly("host.f00.name"); - assertThat(namesFromConfiguration("my.host-name")).toIterable().containsExactly("my.host-name"); - assertThat(namesFromConfiguration("host[FOO].name")).toIterable().containsExactly("host[FOO].name"); + assertThat(mapConfigurationPropertyName("server")).containsExactly("server"); + assertThat(mapConfigurationPropertyName("server.port")).containsExactly("server.port"); + assertThat(mapConfigurationPropertyName("host[0]")).containsExactly("host[0]"); + assertThat(mapConfigurationPropertyName("host[0][1]")).containsExactly("host[0][1]"); + assertThat(mapConfigurationPropertyName("host[0].name")).containsExactly("host[0].name"); + assertThat(mapConfigurationPropertyName("host.f00.name")).containsExactly("host.f00.name"); + assertThat(mapConfigurationPropertyName("my.host-name")).containsExactly("my.host-name"); + assertThat(mapConfigurationPropertyName("host[FOO].name")).containsExactly("host[FOO].name"); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCacheTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCacheTests.java new file mode 100644 index 00000000000..581b2892cdb --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCacheTests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SoftReferenceConfigurationPropertyCache}. + * + * @author Phillip Webb + */ +class SoftReferenceConfigurationPropertyCacheTests { + + private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2020-01-02T09:00:00Z"), ZoneOffset.UTC); + + private Clock clock = FIXED_CLOCK; + + private AtomicInteger createCount = new AtomicInteger(); + + private TestSoftReferenceConfigurationPropertyCache cache = new TestSoftReferenceConfigurationPropertyCache(false); + + @Test + void getReturnsValueWithCorrectCounts() { + get(this.cache).assertCounts(0, 0); + get(this.cache).assertCounts(0, 1); + get(this.cache).assertCounts(0, 2); + } + + @Test + void getWhenNeverExpireReturnsValueWithCorrectCounts() { + this.cache = new TestSoftReferenceConfigurationPropertyCache(true); + get(this.cache).assertCounts(0, 0); + get(this.cache).assertCounts(0, 0); + get(this.cache).assertCounts(0, 0); + } + + @Test + void enableEnablesCachingWithUnlimtedTimeToLive() { + this.cache.enable(); + get(this.cache).assertCounts(0, 0); + tick(Duration.ofDays(300)); + get(this.cache).assertCounts(0, 0); + } + + @Test + void setTimeToLiveEnablesCachingWithTimeToLive() { + this.cache.setTimeToLive(Duration.ofDays(1)); + get(this.cache).assertCounts(0, 0); + tick(Duration.ofHours(2)); + get(this.cache).assertCounts(0, 0); + tick(Duration.ofDays(2)); + get(this.cache).assertCounts(0, 1); + } + + @Test + void setTimeToLiveWhenZeroDisablesCaching() { + this.cache.setTimeToLive(Duration.ZERO); + get(this.cache).assertCounts(0, 0); + get(this.cache).assertCounts(0, 1); + get(this.cache).assertCounts(0, 2); + } + + @Test + void setTimeToLiveWhenNullDisablesCaching() { + this.cache.setTimeToLive(null); + get(this.cache).assertCounts(0, 0); + get(this.cache).assertCounts(0, 1); + get(this.cache).assertCounts(0, 2); + } + + @Test + void clearExpiresCache() { + this.cache.enable(); + get(this.cache).assertCounts(0, 0); + get(this.cache).assertCounts(0, 0); + this.cache.clear(); + get(this.cache).assertCounts(0, 1); + + } + + private Value get(SoftReferenceConfigurationPropertyCache cache) { + return cache.get(this::createValue, this::updateValue); + } + + private Value createValue() { + return new Value(this.createCount.getAndIncrement(), -1); + } + + private Value updateValue(Value value) { + return new Value(value.createCount, value.refreshCount + 1); + } + + private void tick(Duration duration) { + this.clock = Clock.offset(this.clock, duration); + } + + /** + * Testable {@link SoftReferenceConfigurationPropertyCache} that actually uses real + * references. + */ + class TestSoftReferenceConfigurationPropertyCache extends SoftReferenceConfigurationPropertyCache { + + private Value value; + + TestSoftReferenceConfigurationPropertyCache(boolean neverExpire) { + super(neverExpire); + } + + @Override + protected Value getValue() { + return this.value; + } + + @Override + protected void setValue(Value value) { + this.value = value; + } + + @Override + protected Instant now() { + return SoftReferenceConfigurationPropertyCacheTests.this.clock.instant(); + } + + } + + /** + * Value used for testing. + */ + static class Value { + + private final int createCount; + + private int refreshCount; + + Value(int createCount, int refreshCount) { + this.createCount = createCount; + this.refreshCount = refreshCount; + } + + void assertCounts(int expectedCreateCount, int expectedRefreshCount) { + assertThat(this.createCount).as("created").isEqualTo(expectedCreateCount); + assertThat(this.refreshCount).as("refreshed").isEqualTo(expectedRefreshCount); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySourceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySourceTests.java index 722718b0ac4..d72e6da2d3d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySourceTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -41,17 +41,10 @@ class SpringConfigurationPropertySourceTests { @Test void createWhenPropertySourceIsNullShouldThrowException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new SpringConfigurationPropertySource(null, mock(PropertyMapper.class), null)) + .isThrownBy(() -> new SpringConfigurationPropertySource(null, mock(PropertyMapper.class))) .withMessageContaining("PropertySource must not be null"); } - @Test - void createWhenMapperIsNullShouldThrowException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new SpringConfigurationPropertySource(mock(PropertySource.class), null, null)) - .withMessageContaining("Mapper must not be null"); - } - @Test void getValueShouldUseDirectMapping() { Map source = new LinkedHashMap<>(); @@ -62,7 +55,7 @@ class SpringConfigurationPropertySourceTests { TestPropertyMapper mapper = new TestPropertyMapper(); ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); mapper.addFromConfigurationProperty(name, "key2"); - SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper, null); + SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper); assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("value2"); } @@ -74,7 +67,7 @@ class SpringConfigurationPropertySourceTests { TestPropertyMapper mapper = new TestPropertyMapper(); ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); mapper.addFromConfigurationProperty(name, "key"); - SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper, null); + SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper); assertThat(adapter.getConfigurationProperty(name).getOrigin().toString()) .isEqualTo("\"key\" from property source \"test\""); } @@ -87,7 +80,7 @@ class SpringConfigurationPropertySourceTests { TestPropertyMapper mapper = new TestPropertyMapper(); ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); mapper.addFromConfigurationProperty(name, "key"); - SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper, null); + SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper); assertThat(adapter.getConfigurationProperty(name).getOrigin().toString()).isEqualTo("TestOrigin key"); } @@ -97,7 +90,7 @@ class SpringConfigurationPropertySourceTests { source.put("foo.bar", "value"); PropertySource propertySource = new MapPropertySource("test", source); SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, - DefaultPropertyMapper.INSTANCE, null); + DefaultPropertyMapper.INSTANCE); assertThat(adapter.containsDescendantOf(ConfigurationPropertyName.of("foo"))) .isEqualTo(ConfigurationPropertyState.UNKNOWN); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySourceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySourceTests.java index fa3670d9756..f9b78e5aaee 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySourceTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySourceTests.java @@ -54,14 +54,6 @@ class SpringIterableConfigurationPropertySourceTests { .withMessageContaining("PropertySource must not be null"); } - @Test - void createWhenMapperIsNullShouldThrowException() { - assertThatIllegalArgumentException() - .isThrownBy( - () -> new SpringIterableConfigurationPropertySource(mock(EnumerablePropertySource.class), null)) - .withMessageContaining("Mapper must not be null"); - } - @Test void iteratorShouldAdaptNames() { Map source = new LinkedHashMap<>(); @@ -70,14 +62,16 @@ class SpringIterableConfigurationPropertySourceTests { source.put("key3", "value3"); source.put("key4", "value4"); EnumerablePropertySource propertySource = new MapPropertySource("test", source); - TestPropertyMapper mapper = new TestPropertyMapper(); - mapper.addFromPropertySource("key1", "my.key1"); - mapper.addFromPropertySource("key2", "my.key2a", "my.key2b"); - mapper.addFromPropertySource("key4", "my.key4"); + TestPropertyMapper mapper1 = new TestPropertyMapper(); + mapper1.addFromPropertySource("key1", "my.key1"); + mapper1.addFromPropertySource("key2", "my.key2a"); + mapper1.addFromPropertySource("key4", "my.key4"); + TestPropertyMapper mapper2 = new TestPropertyMapper(); + mapper2.addFromPropertySource("key2", "my.key2b"); SpringIterableConfigurationPropertySource adapter = new SpringIterableConfigurationPropertySource( - propertySource, mapper); + propertySource, mapper1, mapper2); assertThat(adapter.iterator()).toIterable().extracting(Object::toString).containsExactly("my.key1", "my.key2a", - "my.key2b", "my.key4"); + "my.key4"); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java index 0800f12e33d..ee33af9c0c1 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -35,44 +35,36 @@ class SystemEnvironmentPropertyMapperTests extends AbstractPropertyMapperTests { @Test void mapFromStringShouldReturnBestGuess() { - assertThat(namesFromString("SERVER")).toIterable().containsExactly("server"); - assertThat(namesFromString("SERVER_PORT")).toIterable().containsExactly("server.port"); - assertThat(namesFromString("HOST_0")).toIterable().containsExactly("host[0]"); - assertThat(namesFromString("HOST_0_1")).toIterable().containsExactly("host[0][1]"); - assertThat(namesFromString("HOST_0_NAME")).toIterable().containsExactly("host[0].name"); - assertThat(namesFromString("HOST_F00_NAME")).toIterable().containsExactly("host.f00.name"); - assertThat(namesFromString("S-ERVER")).toIterable().containsExactly("s-erver"); + assertThat(mapPropertySourceName("SERVER")).isEqualTo("server"); + assertThat(mapPropertySourceName("SERVER_PORT")).isEqualTo("server.port"); + assertThat(mapPropertySourceName("HOST_0")).isEqualTo("host[0]"); + assertThat(mapPropertySourceName("HOST_0_1")).isEqualTo("host[0][1]"); + assertThat(mapPropertySourceName("HOST_0_NAME")).isEqualTo("host[0].name"); + assertThat(mapPropertySourceName("HOST_F00_NAME")).isEqualTo("host.f00.name"); + assertThat(mapPropertySourceName("S-ERVER")).isEqualTo("s-erver"); } @Test void mapFromConfigurationShouldReturnBestGuess() { - assertThat(namesFromConfiguration("server")).toIterable().containsExactly("SERVER"); - assertThat(namesFromConfiguration("server.port")).toIterable().containsExactly("SERVER_PORT"); - assertThat(namesFromConfiguration("host[0]")).toIterable().containsExactly("HOST_0"); - assertThat(namesFromConfiguration("host[0][1]")).toIterable().containsExactly("HOST_0_1"); - assertThat(namesFromConfiguration("host[0].name")).toIterable().containsExactly("HOST_0_NAME"); - assertThat(namesFromConfiguration("host.f00.name")).toIterable().containsExactly("HOST_F00_NAME"); - assertThat(namesFromConfiguration("foo.the-bar")).toIterable().containsExactly("FOO_THEBAR", "FOO_THE_BAR"); + assertThat(mapConfigurationPropertyName("server")).containsExactly("SERVER"); + assertThat(mapConfigurationPropertyName("server.port")).containsExactly("SERVER_PORT"); + assertThat(mapConfigurationPropertyName("host[0]")).containsExactly("HOST_0"); + assertThat(mapConfigurationPropertyName("host[0][1]")).containsExactly("HOST_0_1"); + assertThat(mapConfigurationPropertyName("host[0].name")).containsExactly("HOST_0_NAME"); + assertThat(mapConfigurationPropertyName("host.f00.name")).containsExactly("HOST_F00_NAME"); + assertThat(mapConfigurationPropertyName("foo.the-bar")).containsExactly("FOO_THEBAR", "FOO_THE_BAR"); } @Test - void underscoreShouldNotMapToEmptyString() { - PropertyMapping[] mappings = getMapper().map("_"); - boolean applicable = false; - for (PropertyMapping mapping : mappings) { - applicable = mapping.isApplicable(ConfigurationPropertyName.of("")); - } - assertThat(applicable).isFalse(); + void underscoreShouldMapToEmptyString() { + ConfigurationPropertyName mapped = getMapper().map("_"); + assertThat(mapped.isEmpty()).isTrue(); } @Test - void underscoreWithWhitespaceShouldNotMapToEmptyString() { - PropertyMapping[] mappings = getMapper().map(" _"); - boolean applicable = false; - for (PropertyMapping mapping : mappings) { - applicable = mapping.isApplicable(ConfigurationPropertyName.of("")); - } - assertThat(applicable).isFalse(); + void underscoreWithWhitespaceShouldMapToEmptyString() { + ConfigurationPropertyName mapped = getMapper().map(" _"); + assertThat(mapped.isEmpty()).isTrue(); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/TestPropertyMapper.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/TestPropertyMapper.java index adac69356e9..0ca1a56a72e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/TestPropertyMapper.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/TestPropertyMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -17,6 +17,9 @@ 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.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -26,33 +29,28 @@ import org.springframework.util.MultiValueMap; */ class TestPropertyMapper implements PropertyMapper { - private MultiValueMap fromSource = new LinkedMultiValueMap<>(); + private MultiValueMap fromConfig = new LinkedMultiValueMap<>(); - private MultiValueMap fromConfig = new LinkedMultiValueMap<>(); + private Map fromSource = new LinkedHashMap<>(); - void addFromPropertySource(String from, String... to) { - for (String configurationPropertyName : to) { - this.fromSource.add(from, - new PropertyMapping(from, ConfigurationPropertyName.of(configurationPropertyName))); - } + void addFromPropertySource(String from, String to) { + this.fromSource.put(from, ConfigurationPropertyName.of(to)); } void addFromConfigurationProperty(ConfigurationPropertyName from, String... to) { for (String propertySourceName : to) { - this.fromConfig.add(from, new PropertyMapping(propertySourceName, from)); + this.fromConfig.add(from, propertySourceName); } } @Override - public PropertyMapping[] map(String propertySourceName) { - return this.fromSource.getOrDefault(propertySourceName, Collections.emptyList()) - .toArray(new PropertyMapping[0]); + public List map(ConfigurationPropertyName configurationPropertyName) { + return this.fromConfig.getOrDefault(configurationPropertyName, Collections.emptyList()); } @Override - public PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName) { - return this.fromConfig.getOrDefault(configurationPropertyName, Collections.emptyList()) - .toArray(new PropertyMapping[0]); + public ConfigurationPropertyName map(String propertySourceName) { + return this.fromSource.getOrDefault(propertySourceName, ConfigurationPropertyName.EMPTY); } @Override