Add ConfigurationPropertyCaching support

Add a `ConfigurationPropertyCaching` utility interface that can be
used to control the property source caching.

Prior to this commit, a `ConfigurationPropertySource` that was backed
by a mutable `EnumerablePropertySource` would need to call the
`getPropertyNames()` method each time a property was accessed. Since
this this operation can be expensive, we now provide a way to cache
the results for a specific length of time.

This commit also improves the performance of immutable property sources
by limiting the number of candidates that need to be searched.
Previously, all mapped names would be enumerated. Now, mappings are
grouped by `ConfigurationPropertyName`. This is especially helpful when
the `ConfigurationPropertyName` isn't mapped at all since the hash based
map lookup will be very fast and the resulting mappings will be empty.

Closes gh-20625
This commit is contained in:
Phillip Webb 2020-05-04 22:40:48 -07:00
parent 85e9a73e85
commit 7afd25fc9a
23 changed files with 1125 additions and 529 deletions

View File

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

View File

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

View File

@ -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<ConfigurationPropertySource> sources;
ConfigurationPropertySourcesCaching(Iterable<ConfigurationPropertySource> 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<ConfigurationPropertyCaching> action) {
if (this.sources != null) {
for (ConfigurationPropertySource source : this.sources) {
ConfigurationPropertyCaching caching = CachingConfigurationPropertySource.find(source);
if (caching != null) {
action.accept(caching);
}
}
}
}
}

View File

@ -16,6 +16,9 @@
package org.springframework.boot.context.properties.source; package org.springframework.boot.context.properties.source;
import java.util.Collections;
import java.util.List;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
@ -32,48 +35,48 @@ final class DefaultPropertyMapper implements PropertyMapper {
public static final PropertyMapper INSTANCE = new DefaultPropertyMapper(); public static final PropertyMapper INSTANCE = new DefaultPropertyMapper();
private LastMapping<ConfigurationPropertyName> lastMappedConfigurationPropertyName; private LastMapping<ConfigurationPropertyName, List<String>> lastMappedConfigurationPropertyName;
private LastMapping<String> lastMappedPropertyName; private LastMapping<String, ConfigurationPropertyName> lastMappedPropertyName;
private DefaultPropertyMapper() { private DefaultPropertyMapper() {
} }
@Override @Override
public PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName) { public List<String> map(ConfigurationPropertyName configurationPropertyName) {
// Use a local copy in case another thread changes things // Use a local copy in case another thread changes things
LastMapping<ConfigurationPropertyName> last = this.lastMappedConfigurationPropertyName; LastMapping<ConfigurationPropertyName, List<String>> last = this.lastMappedConfigurationPropertyName;
if (last != null && last.isFrom(configurationPropertyName)) { if (last != null && last.isFrom(configurationPropertyName)) {
return last.getMapping(); return last.getMapping();
} }
String convertedName = configurationPropertyName.toString(); String convertedName = configurationPropertyName.toString();
PropertyMapping[] mapping = { new PropertyMapping(convertedName, configurationPropertyName) }; List<String> mapping = Collections.singletonList(convertedName);
this.lastMappedConfigurationPropertyName = new LastMapping<>(configurationPropertyName, mapping); this.lastMappedConfigurationPropertyName = new LastMapping<>(configurationPropertyName, mapping);
return mapping; return mapping;
} }
@Override @Override
public PropertyMapping[] map(String propertySourceName) { public ConfigurationPropertyName map(String propertySourceName) {
// Use a local copy in case another thread changes things // Use a local copy in case another thread changes things
LastMapping<String> last = this.lastMappedPropertyName; LastMapping<String, ConfigurationPropertyName> last = this.lastMappedPropertyName;
if (last != null && last.isFrom(propertySourceName)) { if (last != null && last.isFrom(propertySourceName)) {
return last.getMapping(); return last.getMapping();
} }
PropertyMapping[] mapping = tryMap(propertySourceName); ConfigurationPropertyName mapping = tryMap(propertySourceName);
this.lastMappedPropertyName = new LastMapping<>(propertySourceName, mapping); this.lastMappedPropertyName = new LastMapping<>(propertySourceName, mapping);
return mapping; return mapping;
} }
private PropertyMapping[] tryMap(String propertySourceName) { private ConfigurationPropertyName tryMap(String propertySourceName) {
try { try {
ConfigurationPropertyName convertedName = ConfigurationPropertyName.adapt(propertySourceName, '.'); ConfigurationPropertyName convertedName = ConfigurationPropertyName.adapt(propertySourceName, '.');
if (!convertedName.isEmpty()) { if (!convertedName.isEmpty()) {
return new PropertyMapping[] { new PropertyMapping(propertySourceName, convertedName) }; return convertedName;
} }
} }
catch (Exception ex) { catch (Exception ex) {
} }
return NO_MAPPINGS; return ConfigurationPropertyName.EMPTY;
} }
@Override @Override
@ -81,13 +84,13 @@ final class DefaultPropertyMapper implements PropertyMapper {
return name.isAncestorOf(candidate); return name.isAncestorOf(candidate);
} }
private static class LastMapping<T> { private static class LastMapping<T, M> {
private final T from; 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.from = from;
this.mapping = mapping; this.mapping = mapping;
} }
@ -96,7 +99,7 @@ final class DefaultPropertyMapper implements PropertyMapper {
return ObjectUtils.nullSafeEquals(from, this.from); return ObjectUtils.nullSafeEquals(from, this.from);
} }
PropertyMapping[] getMapping() { M getMapping() {
return this.mapping; return this.mapping;
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 { public class MapConfigurationPropertySource implements IterableConfigurationPropertySource {
private static final PropertyMapper[] DEFAULT_MAPPERS = { DefaultPropertyMapper.INSTANCE };
private final Map<String, Object> source; private final Map<String, Object> source;
private final IterableConfigurationPropertySource delegate; private final IterableConfigurationPropertySource delegate;
@ -53,8 +55,8 @@ public class MapConfigurationPropertySource implements IterableConfigurationProp
*/ */
public MapConfigurationPropertySource(Map<?, ?> map) { public MapConfigurationPropertySource(Map<?, ?> map) {
this.source = new LinkedHashMap<>(); this.source = new LinkedHashMap<>();
this.delegate = new SpringIterableConfigurationPropertySource(new MapPropertySource("source", this.source), MapPropertySource mapPropertySource = new MapPropertySource("source", this.source);
DefaultPropertyMapper.INSTANCE); this.delegate = new SpringIterableConfigurationPropertySource(mapPropertySource, DEFAULT_MAPPERS);
putAll(map); putAll(map);
} }

View File

@ -16,6 +16,8 @@
package org.springframework.boot.context.properties.source; package org.springframework.boot.context.properties.source;
import java.util.List;
import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertySource; 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. * {@link SpringConfigurationPropertySource} to first attempt any direct mappings (i.e.
* map the {@link ConfigurationPropertyName} directly to the {@link PropertySource} name) * map the {@link ConfigurationPropertyName} directly to the {@link PropertySource} name)
* before falling back to {@link EnumerablePropertySource enumerating} property names, * before falling back to {@link EnumerablePropertySource enumerating} property names,
* mapping them to a {@link ConfigurationPropertyName} and checking for * mapping them to a {@link ConfigurationPropertyName} and checking for applicability. See
* {@link PropertyMapping#isApplicable(ConfigurationPropertyName) applicability}. See
* {@link SpringConfigurationPropertySource} for more details. * {@link SpringConfigurationPropertySource} for more details.
* *
* @author Phillip Webb * @author Phillip Webb
@ -38,22 +39,21 @@ import org.springframework.core.env.PropertySource;
*/ */
interface PropertyMapper { interface PropertyMapper {
PropertyMapping[] NO_MAPPINGS = {};
/** /**
* Provide mappings from a {@link ConfigurationPropertySource} * Provide mappings from a {@link ConfigurationPropertySource}
* {@link ConfigurationPropertyName}. * {@link ConfigurationPropertyName}.
* @param configurationPropertyName the name to map * @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<String> map(ConfigurationPropertyName configurationPropertyName);
/** /**
* Provide mappings from a {@link PropertySource} property name. * Provide mappings from a {@link PropertySource} property name.
* @param propertySourceName the name to map * @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 * Returns {@code true} if {@code name} is an ancestor (immediate or nested parent) of

View File

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

View File

@ -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 <T> the value type
* @author Phillip Webb
*/
class SoftReferenceConfigurationPropertyCache<T> implements ConfigurationPropertyCaching {
private static final Duration UNLIMITED = Duration.ZERO;
private final boolean neverExpire;
private volatile Duration timeToLive;
private volatile SoftReference<T> 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<T> factory, UnaryOperator<T> 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);
}
}

View File

@ -18,7 +18,6 @@ package org.springframework.boot.context.properties.source;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.function.Function;
import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.PropertySourceOrigin; 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.StandardEnvironment;
import org.springframework.core.env.SystemEnvironmentPropertySource; import org.springframework.core.env.SystemEnvironmentPropertySource;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/** /**
* {@link ConfigurationPropertySource} backed by a non-enumerable Spring * {@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 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 PropertySource<?> propertySource;
private final PropertyMapper mapper; private final PropertyMapper[] mappers;
private final Function<ConfigurationPropertyName, ConfigurationPropertyState> containsDescendantOf;
/** /**
* Create a new {@link SpringConfigurationPropertySource} implementation. * Create a new {@link SpringConfigurationPropertySource} implementation.
* @param propertySource the source property source * @param propertySource the source property source
* @param mapper the property mapper * @param mappers the property mappers
* @param containsDescendantOf function used to implement
* {@link #containsDescendantOf(ConfigurationPropertyName)} (may be {@code null})
*/ */
SpringConfigurationPropertySource(PropertySource<?> propertySource, PropertyMapper mapper, SpringConfigurationPropertySource(PropertySource<?> propertySource, PropertyMapper... mappers) {
Function<ConfigurationPropertyName, ConfigurationPropertyState> containsDescendantOf) {
Assert.notNull(propertySource, "PropertySource must not be null"); Assert.notNull(propertySource, "PropertySource must not be null");
Assert.notNull(mapper, "Mapper must not be null");
this.propertySource = propertySource; this.propertySource = propertySource;
this.mapper = (mapper instanceof DelegatingPropertyMapper) ? mapper : new DelegatingPropertyMapper(mapper); this.mappers = mappers;
this.containsDescendantOf = (containsDescendantOf != null) ? containsDescendantOf
: (n) -> ConfigurationPropertyState.UNKNOWN;
} }
@Override @Override
public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) { public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {
PropertyMapping[] mappings = getMapper().map(name); if (name == null) {
return find(mappings, name); 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 @Override
public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { 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 @Override
@ -94,35 +114,12 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource {
return this.propertySource; 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() { protected PropertySource<?> getPropertySource() {
return this.propertySource; return this.propertySource;
} }
protected final PropertyMapper getMapper() { protected final PropertyMapper[] getMappers() {
return this.mapper; return this.mappers;
} }
@Override @Override
@ -139,19 +136,18 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource {
*/ */
static SpringConfigurationPropertySource from(PropertySource<?> source) { static SpringConfigurationPropertySource from(PropertySource<?> source) {
Assert.notNull(source, "Source must not be null"); Assert.notNull(source, "Source must not be null");
PropertyMapper mapper = getPropertyMapper(source); PropertyMapper[] mappers = getPropertyMappers(source);
if (isFullEnumerable(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)) { if (source instanceof SystemEnvironmentPropertySource && hasSystemEnvironmentName(source)) {
return new DelegatingPropertyMapper(SystemEnvironmentPropertyMapper.INSTANCE, return SYSTEM_ENVIRONMENT_MAPPERS;
DefaultPropertyMapper.INSTANCE);
} }
return new DelegatingPropertyMapper(DefaultPropertyMapper.INSTANCE); return DEFAULT_MAPPERS;
} }
private static boolean hasSystemEnvironmentName(PropertySource<?> source) { private static boolean hasSystemEnvironmentName(PropertySource<?> source) {
@ -181,97 +177,4 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource {
return source; return source;
} }
private static Function<ConfigurationPropertyName, ConfigurationPropertyState> 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;
}
}
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -17,21 +17,27 @@
package org.springframework.boot.context.properties.source; package org.springframework.boot.context.properties.source;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.ConcurrentModificationException; import java.util.ConcurrentModificationException;
import java.util.HashSet; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginLookup; import org.springframework.boot.origin.OriginLookup;
import org.springframework.boot.origin.PropertySourceOrigin;
import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.env.SystemEnvironmentPropertySource; 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}. * {@link ConfigurationPropertySource} backed by an {@link EnumerablePropertySource}.
@ -45,13 +51,16 @@ import org.springframework.util.ObjectUtils;
* @see PropertyMapper * @see PropertyMapper
*/ */
class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySource class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySource
implements IterableConfigurationPropertySource { implements IterableConfigurationPropertySource, CachingConfigurationPropertySource {
private volatile Cache cache; private volatile Collection<ConfigurationPropertyName> configurationPropertyNames;
SpringIterableConfigurationPropertySource(EnumerablePropertySource<?> propertySource, PropertyMapper mapper) { private final SoftReferenceConfigurationPropertyCache<Mappings> cache;
super(propertySource, mapper, null);
SpringIterableConfigurationPropertySource(EnumerablePropertySource<?> propertySource, PropertyMapper... mappers) {
super(propertySource, mappers);
assertEnumerablePropertySource(); assertEnumerablePropertySource();
this.cache = new SoftReferenceConfigurationPropertyCache<>(isImmutablePropertySource());
} }
private void assertEnumerablePropertySource() { private void assertEnumerablePropertySource() {
@ -66,13 +75,28 @@ class SpringIterableConfigurationPropertySource extends SpringConfigurationPrope
} }
@Override @Override
public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) { public ConfigurationPropertyCaching getCaching() {
ConfigurationProperty configurationProperty = super.getConfigurationProperty(name); return this.cache;
if (configurationProperty == null) {
configurationProperty = find(getPropertyMappings(getCache()), name);
} }
@Override
public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {
if (name == null) {
return null;
}
ConfigurationProperty configurationProperty = super.getConfigurationProperty(name);
if (configurationProperty != null) {
return configurationProperty; 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 @Override
public Stream<ConfigurationPropertyName> stream() { public Stream<ConfigurationPropertyName> stream() {
@ -84,158 +108,52 @@ class SpringIterableConfigurationPropertySource extends SpringConfigurationPrope
return getConfigurationPropertyNames().iterator(); return getConfigurationPropertyNames().iterator();
} }
private Collection<ConfigurationPropertyName> getConfigurationPropertyNames() {
if (!isImmutablePropertySource()) {
return getMappings().getConfigurationPropertyNames(getPropertySource().getPropertyNames());
}
Collection<ConfigurationPropertyName> configurationPropertyNames = this.configurationPropertyNames;
if (configurationPropertyNames == null) {
configurationPropertyNames = getMappings()
.getConfigurationPropertyNames(getPropertySource().getPropertyNames());
this.configurationPropertyNames = configurationPropertyNames;
}
return configurationPropertyNames;
}
@Override @Override
public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) {
return ConfigurationPropertyState.search(this, (candidate) -> getMapper().isAncestorOf(name, candidate)); ConfigurationPropertyState result = super.containsDescendantOf(name);
} if (result == ConfigurationPropertyState.UNKNOWN) {
result = ConfigurationPropertyState.search(this, (candidate) -> isAncestorOf(name, candidate));
private List<ConfigurationPropertyName> getConfigurationPropertyNames() {
Cache cache = getCache();
List<ConfigurationPropertyName> 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<PropertyMapping> 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);
} }
return result; return result;
} }
private Cache getCache() { private boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) {
CacheKey key = CacheKey.get(getPropertySource()); for (PropertyMapper mapper : getMappers()) {
if (key == null) { if (mapper.isAncestorOf(name, candidate)) {
return null;
}
Cache cache = this.cache;
try {
if (cache != null && cache.hasKeyEqualTo(key)) {
return cache;
}
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;
}
}
@Override
protected EnumerablePropertySource<?> getPropertySource() {
return (EnumerablePropertySource<?>) super.getPropertySource();
}
private static class Cache {
private final CacheKey key;
private List<ConfigurationPropertyName> names;
private PropertyMapping[] mappings;
Cache(CacheKey key) {
this.key = key;
}
boolean hasKeyEqualTo(CacheKey key) {
return this.key.equals(key);
}
List<ConfigurationPropertyName> getNames() {
return this.names;
}
void setNames(List<ConfigurationPropertyName> names) {
this.names = names;
}
PropertyMapping[] getMappings() {
return this.mappings;
}
void setMappings(PropertyMapping[] mappings) {
this.mappings = mappings;
}
}
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;
}
CacheKey copy() {
if (this == IMMUTABLE_PROPERTY_SOURCE) {
return IMMUTABLE_PROPERTY_SOURCE;
}
return new CacheKey(copyKey(this.key));
}
private Object copyKey(Object key) {
if (key instanceof Set) {
return new HashSet<Object>((Set<?>) key);
}
return ((String[]) key).clone();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true; return true;
} }
if (obj == null || getClass() != obj.getClass()) { }
return false; return false;
} }
CacheKey otherCacheKey = (CacheKey) obj;
return ObjectUtils.nullSafeEquals(this.key, otherCacheKey.key); private Mappings getMappings() {
return this.cache.get(this::createMappings, this::updateMappings);
} }
@Override private Mappings createMappings() {
public int hashCode() { return new Mappings(getMappers(), isImmutablePropertySource());
return this.key.hashCode();
} }
static CacheKey get(EnumerablePropertySource<?> source) { private Mappings updateMappings(Mappings mappings) {
if (isImmutable(source)) { mappings.updateMappings(getPropertySource()::getPropertyNames);
return IMMUTABLE_PROPERTY_SOURCE; return mappings;
}
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) { private boolean isImmutablePropertySource() {
EnumerablePropertySource<?> source = getPropertySource();
if (source instanceof OriginLookup) { if (source instanceof OriginLookup) {
return ((OriginLookup<?>) source).isImmutable(); return ((OriginLookup<?>) source).isImmutable();
} }
@ -245,6 +163,99 @@ class SpringIterableConfigurationPropertySource extends SpringConfigurationPrope
return false; return false;
} }
@Override
protected EnumerablePropertySource<?> getPropertySource() {
return (EnumerablePropertySource<?>) super.getPropertySource();
}
private static class Mappings {
private final PropertyMapper[] mappers;
private final boolean immutable;
private volatile MultiValueMap<ConfigurationPropertyName, String> mappings;
private volatile Map<String, ConfigurationPropertyName> reverseMappings;
private volatile Collection<ConfigurationPropertyName> configurationPropertyNames;
private volatile String[] lastUpdated;
Mappings(PropertyMapper[] mappers, boolean immutable) {
this.mappers = mappers;
this.immutable = immutable;
}
void updateMappings(Supplier<String[]> 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;
}
}
}
}
}
private void updateMappings(String[] propertyNames) {
String[] lastUpdated = this.lastUpdated;
if (lastUpdated != null && Arrays.equals(lastUpdated, propertyNames)) {
return;
}
MultiValueMap<ConfigurationPropertyName, String> previousMappings = this.mappings;
MultiValueMap<ConfigurationPropertyName, String> mappings = (previousMappings != null)
? new LinkedMultiValueMap<>(previousMappings) : new LinkedMultiValueMap<>(propertyNames.length);
Map<String, ConfigurationPropertyName> previousReverseMappings = this.reverseMappings;
Map<String, ConfigurationPropertyName> 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;
}
List<String> getMapped(ConfigurationPropertyName configurationPropertyName) {
return this.mappings.getOrDefault(configurationPropertyName, Collections.emptyList());
}
Collection<ConfigurationPropertyName> getConfigurationPropertyNames(String[] propertyNames) {
Collection<ConfigurationPropertyName> names = this.configurationPropertyNames;
if (names != null) {
return names;
}
Map<String, ConfigurationPropertyName> reverseMappings = this.reverseMappings;
if (reverseMappings == null || reverseMappings.isEmpty()) {
return Collections.emptySet();
}
List<ConfigurationPropertyName> relevantNames = new ArrayList<>(reverseMappings.size());
for (String propertyName : propertyNames) {
ConfigurationPropertyName configurationPropertyName = reverseMappings.get(propertyName);
if (configurationPropertyName != null) {
relevantNames.add(configurationPropertyName);
}
}
return relevantNames;
}
} }
} }

View File

@ -16,6 +16,9 @@
package org.springframework.boot.context.properties.source; package org.springframework.boot.context.properties.source;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; 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(); public static final PropertyMapper INSTANCE = new SystemEnvironmentPropertyMapper();
@Override @Override
public PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName) { public List<String> map(ConfigurationPropertyName configurationPropertyName) {
String name = convertName(configurationPropertyName); String name = convertName(configurationPropertyName);
String legacyName = convertLegacyName(configurationPropertyName); String legacyName = convertLegacyName(configurationPropertyName);
if (name.equals(legacyName)) { if (name.equals(legacyName)) {
return new PropertyMapping[] { new PropertyMapping(name, configurationPropertyName) }; return Collections.singletonList(name);
}
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 Arrays.asList(name, legacyName);
} }
private String convertName(ConfigurationPropertyName name) { private String convertName(ConfigurationPropertyName name) {
@ -122,6 +79,20 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper {
return element.replace('-', '_').toUpperCase(Locale.ENGLISH); 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) { private CharSequence processElementValue(CharSequence value) {
String result = value.toString().toLowerCase(Locale.ENGLISH); String result = value.toString().toLowerCase(Locale.ENGLISH);
return isNumber(result) ? "[" + result + "]" : result; return isNumber(result) ? "[" + result + "]" : result;
@ -131,4 +102,31 @@ final class SystemEnvironmentPropertyMapper implements PropertyMapper {
return string.chars().allMatch(Character::isDigit); 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;
}
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.boot.context.properties.source;
import java.util.Arrays; import java.util.List;
import java.util.Iterator;
/** /**
* Abstract base class for {@link PropertyMapper} tests. * Abstract base class for {@link PropertyMapper} tests.
@ -30,14 +29,12 @@ public abstract class AbstractPropertyMapperTests {
protected abstract PropertyMapper getMapper(); protected abstract PropertyMapper getMapper();
protected final Iterator<String> namesFromString(String name) { protected final List<String> mapConfigurationPropertyName(String configurationPropertyName) {
return Arrays.stream(getMapper().map(name)).map((mapping) -> mapping.getConfigurationPropertyName().toString()) return getMapper().map(ConfigurationPropertyName.of(configurationPropertyName));
.iterator();
} }
protected final Iterator<String> namesFromConfiguration(String name) { protected final String mapPropertySourceName(String propertySourceName) {
return Arrays.stream(getMapper().map(ConfigurationPropertyName.of(name))) return getMapper().map(propertySourceName).toString();
.map(PropertyMapping::getPropertySourceName).iterator();
} }
} }

View File

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

View File

@ -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<ConfigurationPropertySource>) null))
.withMessage("Sources must not be null");
}
@Test
void getFromSourcesReturnsCachingComposite() {
List<ConfigurationPropertySource> sources = new ArrayList<>();
sources.add(SpringConfigurationPropertySource.from(this.propertySource));
ConfigurationPropertyCaching caching = ConfigurationPropertyCaching.get(sources);
assertThat(caching).isInstanceOf(ConfigurationPropertySourcesCaching.class);
}
@Test
void getFromSourcesForUnderlyingSourceReturnsCaching() {
List<ConfigurationPropertySource> 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<ConfigurationPropertySource> 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");
}
}

View File

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

View File

@ -130,6 +130,13 @@ class ConfigurationPropertySourcesTests {
testPropertySourcePerformance(true, 1000); testPropertySourcePerformance(true, 1000);
} }
@Test // gh-20625
void environmentPropertyAccessWhenMutableWithCacheShouldBePerformant() {
StandardEnvironment environment = createPerformanceTestEnvironment(false);
ConfigurationPropertyCaching.get(environment).enable();
testPropertySourcePerformance(environment, 1000);
}
@Test // gh-20625 @Test // gh-20625
@Disabled("for manual testing") @Disabled("for manual testing")
void environmentPropertyAccessWhenMutableShouldBeTolerable() { void environmentPropertyAccessWhenMutableShouldBeTolerable() {

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -35,29 +35,29 @@ class DefaultPropertyMapperTests extends AbstractPropertyMapperTests {
@Test @Test
void mapFromStringShouldReturnBestGuess() { void mapFromStringShouldReturnBestGuess() {
assertThat(namesFromString("server")).toIterable().containsExactly("server"); assertThat(mapPropertySourceName("server")).isEqualTo("server");
assertThat(namesFromString("server.port")).toIterable().containsExactly("server.port"); assertThat(mapPropertySourceName("server.port")).isEqualTo("server.port");
assertThat(namesFromString("host[0]")).toIterable().containsExactly("host[0]"); assertThat(mapPropertySourceName("host[0]")).isEqualTo("host[0]");
assertThat(namesFromString("host[0][1]")).toIterable().containsExactly("host[0][1]"); assertThat(mapPropertySourceName("host[0][1]")).isEqualTo("host[0][1]");
assertThat(namesFromString("host[0].name")).toIterable().containsExactly("host[0].name"); assertThat(mapPropertySourceName("host[0].name")).isEqualTo("host[0].name");
assertThat(namesFromString("host.f00.name")).toIterable().containsExactly("host.f00.name"); assertThat(mapPropertySourceName("host.f00.name")).isEqualTo("host.f00.name");
assertThat(namesFromString("my.host-name")).toIterable().containsExactly("my.host-name"); assertThat(mapPropertySourceName("my.host-name")).isEqualTo("my.host-name");
assertThat(namesFromString("my.hostName")).toIterable().containsExactly("my.hostname"); assertThat(mapPropertySourceName("my.hostName")).isEqualTo("my.hostname");
assertThat(namesFromString("my.HOST_NAME")).toIterable().containsExactly("my.hostname"); assertThat(mapPropertySourceName("my.HOST_NAME")).isEqualTo("my.hostname");
assertThat(namesFromString("s[!@#$%^&*()=+]e-rVeR")).toIterable().containsExactly("s[!@#$%^&*()=+].e-rver"); assertThat(mapPropertySourceName("s[!@#$%^&*()=+]e-rVeR")).isEqualTo("s[!@#$%^&*()=+].e-rver");
assertThat(namesFromString("host[FOO].name")).toIterable().containsExactly("host[FOO].name"); assertThat(mapPropertySourceName("host[FOO].name")).isEqualTo("host[FOO].name");
} }
@Test @Test
void mapFromConfigurationShouldReturnBestGuess() { void mapFromConfigurationShouldReturnBestGuess() {
assertThat(namesFromConfiguration("server")).toIterable().containsExactly("server"); assertThat(mapConfigurationPropertyName("server")).containsExactly("server");
assertThat(namesFromConfiguration("server.port")).toIterable().containsExactly("server.port"); assertThat(mapConfigurationPropertyName("server.port")).containsExactly("server.port");
assertThat(namesFromConfiguration("host[0]")).toIterable().containsExactly("host[0]"); assertThat(mapConfigurationPropertyName("host[0]")).containsExactly("host[0]");
assertThat(namesFromConfiguration("host[0][1]")).toIterable().containsExactly("host[0][1]"); assertThat(mapConfigurationPropertyName("host[0][1]")).containsExactly("host[0][1]");
assertThat(namesFromConfiguration("host[0].name")).toIterable().containsExactly("host[0].name"); assertThat(mapConfigurationPropertyName("host[0].name")).containsExactly("host[0].name");
assertThat(namesFromConfiguration("host.f00.name")).toIterable().containsExactly("host.f00.name"); assertThat(mapConfigurationPropertyName("host.f00.name")).containsExactly("host.f00.name");
assertThat(namesFromConfiguration("my.host-name")).toIterable().containsExactly("my.host-name"); assertThat(mapConfigurationPropertyName("my.host-name")).containsExactly("my.host-name");
assertThat(namesFromConfiguration("host[FOO].name")).toIterable().containsExactly("host[FOO].name"); assertThat(mapConfigurationPropertyName("host[FOO].name")).containsExactly("host[FOO].name");
} }
} }

View File

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

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,17 +41,10 @@ class SpringConfigurationPropertySourceTests {
@Test @Test
void createWhenPropertySourceIsNullShouldThrowException() { void createWhenPropertySourceIsNullShouldThrowException() {
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy(() -> new SpringConfigurationPropertySource(null, mock(PropertyMapper.class), null)) .isThrownBy(() -> new SpringConfigurationPropertySource(null, mock(PropertyMapper.class)))
.withMessageContaining("PropertySource must not be null"); .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 @Test
void getValueShouldUseDirectMapping() { void getValueShouldUseDirectMapping() {
Map<String, Object> source = new LinkedHashMap<>(); Map<String, Object> source = new LinkedHashMap<>();
@ -62,7 +55,7 @@ class SpringConfigurationPropertySourceTests {
TestPropertyMapper mapper = new TestPropertyMapper(); TestPropertyMapper mapper = new TestPropertyMapper();
ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key");
mapper.addFromConfigurationProperty(name, "key2"); mapper.addFromConfigurationProperty(name, "key2");
SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper, null); SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper);
assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("value2"); assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("value2");
} }
@ -74,7 +67,7 @@ class SpringConfigurationPropertySourceTests {
TestPropertyMapper mapper = new TestPropertyMapper(); TestPropertyMapper mapper = new TestPropertyMapper();
ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key");
mapper.addFromConfigurationProperty(name, "key"); mapper.addFromConfigurationProperty(name, "key");
SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper, null); SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper);
assertThat(adapter.getConfigurationProperty(name).getOrigin().toString()) assertThat(adapter.getConfigurationProperty(name).getOrigin().toString())
.isEqualTo("\"key\" from property source \"test\""); .isEqualTo("\"key\" from property source \"test\"");
} }
@ -87,7 +80,7 @@ class SpringConfigurationPropertySourceTests {
TestPropertyMapper mapper = new TestPropertyMapper(); TestPropertyMapper mapper = new TestPropertyMapper();
ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key");
mapper.addFromConfigurationProperty(name, "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"); assertThat(adapter.getConfigurationProperty(name).getOrigin().toString()).isEqualTo("TestOrigin key");
} }
@ -97,7 +90,7 @@ class SpringConfigurationPropertySourceTests {
source.put("foo.bar", "value"); source.put("foo.bar", "value");
PropertySource<?> propertySource = new MapPropertySource("test", source); PropertySource<?> propertySource = new MapPropertySource("test", source);
SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource,
DefaultPropertyMapper.INSTANCE, null); DefaultPropertyMapper.INSTANCE);
assertThat(adapter.containsDescendantOf(ConfigurationPropertyName.of("foo"))) assertThat(adapter.containsDescendantOf(ConfigurationPropertyName.of("foo")))
.isEqualTo(ConfigurationPropertyState.UNKNOWN); .isEqualTo(ConfigurationPropertyState.UNKNOWN);
} }

View File

@ -54,14 +54,6 @@ class SpringIterableConfigurationPropertySourceTests {
.withMessageContaining("PropertySource must not be null"); .withMessageContaining("PropertySource must not be null");
} }
@Test
void createWhenMapperIsNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(
() -> new SpringIterableConfigurationPropertySource(mock(EnumerablePropertySource.class), null))
.withMessageContaining("Mapper must not be null");
}
@Test @Test
void iteratorShouldAdaptNames() { void iteratorShouldAdaptNames() {
Map<String, Object> source = new LinkedHashMap<>(); Map<String, Object> source = new LinkedHashMap<>();
@ -70,14 +62,16 @@ class SpringIterableConfigurationPropertySourceTests {
source.put("key3", "value3"); source.put("key3", "value3");
source.put("key4", "value4"); source.put("key4", "value4");
EnumerablePropertySource<?> propertySource = new MapPropertySource("test", source); EnumerablePropertySource<?> propertySource = new MapPropertySource("test", source);
TestPropertyMapper mapper = new TestPropertyMapper(); TestPropertyMapper mapper1 = new TestPropertyMapper();
mapper.addFromPropertySource("key1", "my.key1"); mapper1.addFromPropertySource("key1", "my.key1");
mapper.addFromPropertySource("key2", "my.key2a", "my.key2b"); mapper1.addFromPropertySource("key2", "my.key2a");
mapper.addFromPropertySource("key4", "my.key4"); mapper1.addFromPropertySource("key4", "my.key4");
TestPropertyMapper mapper2 = new TestPropertyMapper();
mapper2.addFromPropertySource("key2", "my.key2b");
SpringIterableConfigurationPropertySource adapter = new SpringIterableConfigurationPropertySource( SpringIterableConfigurationPropertySource adapter = new SpringIterableConfigurationPropertySource(
propertySource, mapper); propertySource, mapper1, mapper2);
assertThat(adapter.iterator()).toIterable().extracting(Object::toString).containsExactly("my.key1", "my.key2a", assertThat(adapter.iterator()).toIterable().extracting(Object::toString).containsExactly("my.key1", "my.key2a",
"my.key2b", "my.key4"); "my.key4");
} }
@Test @Test

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -35,44 +35,36 @@ class SystemEnvironmentPropertyMapperTests extends AbstractPropertyMapperTests {
@Test @Test
void mapFromStringShouldReturnBestGuess() { void mapFromStringShouldReturnBestGuess() {
assertThat(namesFromString("SERVER")).toIterable().containsExactly("server"); assertThat(mapPropertySourceName("SERVER")).isEqualTo("server");
assertThat(namesFromString("SERVER_PORT")).toIterable().containsExactly("server.port"); assertThat(mapPropertySourceName("SERVER_PORT")).isEqualTo("server.port");
assertThat(namesFromString("HOST_0")).toIterable().containsExactly("host[0]"); assertThat(mapPropertySourceName("HOST_0")).isEqualTo("host[0]");
assertThat(namesFromString("HOST_0_1")).toIterable().containsExactly("host[0][1]"); assertThat(mapPropertySourceName("HOST_0_1")).isEqualTo("host[0][1]");
assertThat(namesFromString("HOST_0_NAME")).toIterable().containsExactly("host[0].name"); assertThat(mapPropertySourceName("HOST_0_NAME")).isEqualTo("host[0].name");
assertThat(namesFromString("HOST_F00_NAME")).toIterable().containsExactly("host.f00.name"); assertThat(mapPropertySourceName("HOST_F00_NAME")).isEqualTo("host.f00.name");
assertThat(namesFromString("S-ERVER")).toIterable().containsExactly("s-erver"); assertThat(mapPropertySourceName("S-ERVER")).isEqualTo("s-erver");
} }
@Test @Test
void mapFromConfigurationShouldReturnBestGuess() { void mapFromConfigurationShouldReturnBestGuess() {
assertThat(namesFromConfiguration("server")).toIterable().containsExactly("SERVER"); assertThat(mapConfigurationPropertyName("server")).containsExactly("SERVER");
assertThat(namesFromConfiguration("server.port")).toIterable().containsExactly("SERVER_PORT"); assertThat(mapConfigurationPropertyName("server.port")).containsExactly("SERVER_PORT");
assertThat(namesFromConfiguration("host[0]")).toIterable().containsExactly("HOST_0"); assertThat(mapConfigurationPropertyName("host[0]")).containsExactly("HOST_0");
assertThat(namesFromConfiguration("host[0][1]")).toIterable().containsExactly("HOST_0_1"); assertThat(mapConfigurationPropertyName("host[0][1]")).containsExactly("HOST_0_1");
assertThat(namesFromConfiguration("host[0].name")).toIterable().containsExactly("HOST_0_NAME"); assertThat(mapConfigurationPropertyName("host[0].name")).containsExactly("HOST_0_NAME");
assertThat(namesFromConfiguration("host.f00.name")).toIterable().containsExactly("HOST_F00_NAME"); assertThat(mapConfigurationPropertyName("host.f00.name")).containsExactly("HOST_F00_NAME");
assertThat(namesFromConfiguration("foo.the-bar")).toIterable().containsExactly("FOO_THEBAR", "FOO_THE_BAR"); assertThat(mapConfigurationPropertyName("foo.the-bar")).containsExactly("FOO_THEBAR", "FOO_THE_BAR");
} }
@Test @Test
void underscoreShouldNotMapToEmptyString() { void underscoreShouldMapToEmptyString() {
PropertyMapping[] mappings = getMapper().map("_"); ConfigurationPropertyName mapped = getMapper().map("_");
boolean applicable = false; assertThat(mapped.isEmpty()).isTrue();
for (PropertyMapping mapping : mappings) {
applicable = mapping.isApplicable(ConfigurationPropertyName.of(""));
}
assertThat(applicable).isFalse();
} }
@Test @Test
void underscoreWithWhitespaceShouldNotMapToEmptyString() { void underscoreWithWhitespaceShouldMapToEmptyString() {
PropertyMapping[] mappings = getMapper().map(" _"); ConfigurationPropertyName mapped = getMapper().map(" _");
boolean applicable = false; assertThat(mapped.isEmpty()).isTrue();
for (PropertyMapping mapping : mappings) {
applicable = mapping.isApplicable(ConfigurationPropertyName.of(""));
}
assertThat(applicable).isFalse();
} }
@Test @Test

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.boot.context.properties.source;
import java.util.Collections; 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.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@ -26,33 +29,28 @@ import org.springframework.util.MultiValueMap;
*/ */
class TestPropertyMapper implements PropertyMapper { class TestPropertyMapper implements PropertyMapper {
private MultiValueMap<String, PropertyMapping> fromSource = new LinkedMultiValueMap<>(); private MultiValueMap<ConfigurationPropertyName, String> fromConfig = new LinkedMultiValueMap<>();
private MultiValueMap<ConfigurationPropertyName, PropertyMapping> fromConfig = new LinkedMultiValueMap<>(); private Map<String, ConfigurationPropertyName> fromSource = new LinkedHashMap<>();
void addFromPropertySource(String from, String... to) { void addFromPropertySource(String from, String to) {
for (String configurationPropertyName : to) { this.fromSource.put(from, ConfigurationPropertyName.of(to));
this.fromSource.add(from,
new PropertyMapping(from, ConfigurationPropertyName.of(configurationPropertyName)));
}
} }
void addFromConfigurationProperty(ConfigurationPropertyName from, String... to) { void addFromConfigurationProperty(ConfigurationPropertyName from, String... to) {
for (String propertySourceName : to) { for (String propertySourceName : to) {
this.fromConfig.add(from, new PropertyMapping(propertySourceName, from)); this.fromConfig.add(from, propertySourceName);
} }
} }
@Override @Override
public PropertyMapping[] map(String propertySourceName) { public List<String> map(ConfigurationPropertyName configurationPropertyName) {
return this.fromSource.getOrDefault(propertySourceName, Collections.emptyList()) return this.fromConfig.getOrDefault(configurationPropertyName, Collections.emptyList());
.toArray(new PropertyMapping[0]);
} }
@Override @Override
public PropertyMapping[] map(ConfigurationPropertyName configurationPropertyName) { public ConfigurationPropertyName map(String propertySourceName) {
return this.fromConfig.getOrDefault(configurationPropertyName, Collections.emptyList()) return this.fromSource.getOrDefault(propertySourceName, ConfigurationPropertyName.EMPTY);
.toArray(new PropertyMapping[0]);
} }
@Override @Override