
io.micronaut.context.env.PropertySourcePropertyResolver Maven / Gradle / Ivy
/*
* Copyright 2017-2020 original 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 io.micronaut.context.env;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.format.MapFormat;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.naming.conventions.StringConvention;
import io.micronaut.core.optim.StaticOptimizations;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.EnvironmentProperties;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.value.MapPropertyResolver;
import io.micronaut.core.value.PropertyResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.NOPLogger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A {@link PropertyResolver} that resolves from one or many {@link PropertySource} instances.
*
* @author Graeme Rocher
* @since 1.0
*/
public class PropertySourcePropertyResolver implements PropertyResolver, AutoCloseable {
private static final EnvironmentProperties CURRENT_ENV = StaticOptimizations.get(EnvironmentProperties.class)
.orElseGet(EnvironmentProperties::empty);
private static final Pattern DOT_PATTERN = Pattern.compile("\\.");
private static final Object NO_VALUE = new Object();
private static final PropertyCatalog[] CONVENTIONS = {PropertyCatalog.GENERATED, PropertyCatalog.RAW};
private static final String WILD_CARD_SUFFIX = ".*";
protected final ConversionService conversionService;
protected final PropertyPlaceholderResolver propertyPlaceholderResolver;
protected final Map propertySources = new ConcurrentHashMap<>(10);
// properties are stored in an array of maps organized by character in the alphabet
// this allows optimization of searches by prefix
@SuppressWarnings("MagicNumber")
protected final Map[] catalog = new Map[58];
protected final Map[] rawCatalog = new Map[58];
protected final Map[] nonGenerated = new Map[58];
protected Logger log;
private final Map containsCache = new ConcurrentHashMap<>(20);
/**
* Cache for values before conversion. This avoids recomputing placeholders, which keeps
* random values (e.g. {@code ${random.port}} stable).
*/
private final Map placeholderResolutionCache = new ConcurrentHashMap<>(20);
/**
* Cache for values after conversion.
*/
private final Map resolvedValueCache = new ConcurrentHashMap<>(20);
private final EnvironmentProperties environmentProperties = EnvironmentProperties.fork(CURRENT_ENV);
/**
* Creates a new, initially empty, {@link PropertySourcePropertyResolver} for the given {@link ConversionService}.
*
* @param conversionService The {@link ConversionService}
* @param logEnabled flag to enable or disable logger
*/
public PropertySourcePropertyResolver(ConversionService conversionService, boolean logEnabled) {
this.log = logEnabled ? LoggerFactory.getLogger(getClass()) : NOPLogger.NOP_LOGGER;
this.conversionService = conversionService;
this.propertyPlaceholderResolver = new DefaultPropertyPlaceholderResolver(this, conversionService);
}
/**
* Creates a new, initially empty, {@link PropertySourcePropertyResolver} for the given {@link ConversionService}.
*
* @param conversionService The {@link ConversionService}
*/
public PropertySourcePropertyResolver(ConversionService conversionService) {
this(conversionService, true);
}
/**
* Creates a new, initially empty, {@link PropertySourcePropertyResolver}.
*/
public PropertySourcePropertyResolver() {
this(ConversionService.SHARED);
}
/**
* Creates a new {@link PropertySourcePropertyResolver} for the given {@link PropertySource} instances.
*
* @param propertySources The {@link PropertySource} instances
*/
public PropertySourcePropertyResolver(PropertySource... propertySources) {
this(ConversionService.SHARED);
if (propertySources != null) {
for (PropertySource propertySource : propertySources) {
addPropertySource(propertySource);
}
}
}
/**
* Add a {@link PropertySource} to this resolver.
*
* @param propertySource The {@link PropertySource} to add
* @return This {@link PropertySourcePropertyResolver}
*/
public PropertySourcePropertyResolver addPropertySource(@Nullable PropertySource propertySource) {
if (propertySource != null) {
processPropertySource(propertySource, propertySource.getConvention());
}
return this;
}
/**
* Add a property source for the given map.
*
* @param name The name of the property source
* @param values The values
* @return This environment
*/
public PropertySourcePropertyResolver addPropertySource(String name, @Nullable Map values) {
if (CollectionUtils.isNotEmpty(values)) {
return addPropertySource(PropertySource.of(name, values));
}
return this;
}
@Override
public boolean containsProperty(@Nullable String name) {
if (StringUtils.isEmpty(name)) {
return false;
}
Boolean result = containsCache.get(name);
if (result == null) {
for (PropertyCatalog convention : CONVENTIONS) {
Map entries = resolveEntriesForKey(name, false, convention);
if (entries != null) {
if (entries.containsKey(name)) {
result = true;
break;
}
}
}
if (result == null) {
result = false;
}
containsCache.put(name, result);
}
return result;
}
@Override
public boolean containsProperties(@Nullable String name) {
if (StringUtils.isEmpty(name)) {
return false;
}
for (PropertyCatalog propertyCatalog : CONVENTIONS) {
Map entries = resolveEntriesForKey(name, false, propertyCatalog);
if (entries != null) {
if (entries.containsKey(name)) {
return true;
} else {
String finalName = name + ".";
for (String key : entries.keySet()) {
if (key.startsWith(finalName)) {
return true;
}
}
}
}
}
return false;
}
@NonNull
@Override
public Collection getPropertyEntries(@NonNull String name) {
if (StringUtils.isEmpty(name)) {
return Collections.emptySet();
}
Map entries = resolveEntriesForKey(name, false, PropertyCatalog.NORMALIZED);
if (entries == null) {
return Collections.emptySet();
}
String prefix = name + '.';
Set strings = entries.keySet();
Set result = CollectionUtils.newHashSet(strings.size());
for (String k : strings) {
if (k.startsWith(prefix)) {
String withoutPrefix = k.substring(prefix.length());
int i = withoutPrefix.indexOf('.');
String s;
if (i > -1) {
s = withoutPrefix.substring(0, i);
} else {
s = withoutPrefix;
}
result.add(s);
}
}
return result;
}
@Override
public Set> getPropertyPathMatches(String pathPattern) {
if (StringUtils.isEmpty(pathPattern)) {
return Collections.emptySet();
}
Map entries = resolveEntriesForKey(pathPattern, false, null);
if (entries == null) {
return Collections.emptySet();
}
boolean endsWithWildCard = pathPattern.endsWith(WILD_CARD_SUFFIX);
String resolvedPattern = pathPattern
.replace("[*]", "\\[([\\w\\d-]+?)\\]")
.replace(".*.", "\\.([\\w\\d-]+?)\\.");
if (endsWithWildCard) {
resolvedPattern = resolvedPattern.replace(WILD_CARD_SUFFIX, "\\S*");
} else {
resolvedPattern += "\\S*";
}
Pattern pattern = Pattern.compile(resolvedPattern);
Set keys = entries.keySet();
Set> results = CollectionUtils.newHashSet(keys.size());
for (String key : keys) {
Matcher matcher = pattern.matcher(key);
if (matcher.matches()) {
int i = matcher.groupCount();
if (i > 0) {
if (i == 1) {
results.add(Collections.singletonList(matcher.group(1)));
} else {
List resolved = new ArrayList<>(i);
for (int j = 0; j < i; j++) {
resolved.add(matcher.group(j + 1));
}
results.add(CollectionUtils.unmodifiableList(resolved));
}
}
}
}
return Collections.unmodifiableSet(results);
}
@Override
public @NonNull Map getProperties(String name, StringConvention keyFormat) {
if (StringUtils.isEmpty(name)) {
return Collections.emptyMap();
}
Map entries = resolveEntriesForKey(name, false, keyFormat == StringConvention.RAW ? PropertyCatalog.RAW : PropertyCatalog.GENERATED);
if (entries != null) {
if (keyFormat == null) {
keyFormat = StringConvention.RAW;
}
return resolveSubMap(
name,
entries,
ConversionContext.MAP,
keyFormat,
MapFormat.MapTransformation.FLAT
);
} else {
entries = resolveEntriesForKey(name, false, PropertyCatalog.GENERATED);
if (keyFormat == null) {
keyFormat = StringConvention.RAW;
}
if (entries == null) {
return Collections.emptyMap();
}
return resolveSubMap(
name,
entries,
ConversionContext.MAP,
keyFormat,
MapFormat.MapTransformation.FLAT
);
}
}
@Override
public Optional getProperty(@NonNull String name, @NonNull ArgumentConversionContext conversionContext) {
if (StringUtils.isEmpty(name)) {
return Optional.empty();
}
Objects.requireNonNull(conversionContext, "Conversion context should not be null");
Class requiredType = conversionContext.getArgument().getType();
boolean cacheableType = ClassUtils.isJavaLangType(requiredType);
ConversionCacheKey cacheKey = new ConversionCacheKey(name, requiredType);
Object cached = cacheableType ? resolvedValueCache.get(cacheKey) : null;
if (cached != null) {
return cached == NO_VALUE ? Optional.empty() : Optional.of((T) cached);
}
Object value = placeholderResolutionCache.get(name);
// entries map to get the value from, only populated if there's a cache miss with placeholderResolutionCache
Map entries = null;
if (value == null) {
entries = resolveEntriesForKey(name, false, PropertyCatalog.GENERATED);
if (entries == null) {
entries = resolveEntriesForKey(name, false, PropertyCatalog.RAW);
}
}
if (entries != null || value != null) {
if (value == null) {
value = entries.get(name);
}
if (value == null) {
value = entries.get(normalizeName(name));
if (value == null && name.indexOf('[') == -1) {
// last chance lookup the raw value
Map rawEntries = resolveEntriesForKey(name, false, PropertyCatalog.RAW);
value = rawEntries != null ? rawEntries.get(name) : null;
if (value != null) {
entries = rawEntries;
}
}
}
if (value == null) {
int i = name.indexOf('[');
if (i > -1 && name.endsWith("]")) {
String newKey = name.substring(0, i);
value = entries.get(newKey);
String index = name.substring(i + 1, name.length() - 1);
if (StringUtils.isNotEmpty(index)) {
if (value != null) {
if (value instanceof List> list) {
try {
value = list.get(Integer.parseInt(index));
} catch (NumberFormatException e) {
// ignore
}
} else if (value instanceof Map, ?> map) {
try {
value = map.get(index);
} catch (NumberFormatException e) {
// ignore
}
}
} else {
String subKey = newKey + '.' + index;
value = entries.get(subKey);
}
}
}
}
if (value != null) {
Optional converted;
if (entries != null) {
// iff entries is null, the value is from placeholderResolutionCache and doesn't need this step
value = resolvePlaceHoldersIfNecessary(value);
placeholderResolutionCache.put(name, value);
}
if (requiredType.isInstance(value) && !CollectionUtils.isIterableOrMap(requiredType)) {
converted = (Optional) Optional.of(value);
} else {
converted = conversionService.convert(value, conversionContext);
}
if (log.isTraceEnabled()) {
if (converted.isPresent()) {
log.trace("Resolved value [{}] for property: {}", converted.get(), name);
} else {
log.trace("Resolved value [{}] cannot be converted to type [{}] for property: {}", value, conversionContext.getArgument(), name);
}
}
if (cacheableType) {
resolvedValueCache.put(cacheKey, converted.orElse((T) NO_VALUE));
}
return converted;
} else if (cacheableType) {
resolvedValueCache.put(cacheKey, NO_VALUE);
return Optional.empty();
} else if (Properties.class.isAssignableFrom(requiredType)) {
Properties properties = resolveSubProperties(name, entries, conversionContext);
return Optional.of((T) properties);
} else if (Map.class.isAssignableFrom(requiredType)) {
Map subMap = resolveSubMap(name, entries, conversionContext);
if (!subMap.isEmpty()) {
return conversionService.convert(subMap, Map.class, requiredType, conversionContext);
} else {
return (Optional) Optional.of(subMap);
}
} else if (PropertyResolver.class.isAssignableFrom(requiredType)) {
Map subMap = resolveSubMap(name, entries, conversionContext);
return Optional.of((T) new MapPropertyResolver(subMap, conversionService));
}
}
log.trace("No value found for property: {}", name);
if (Properties.class.isAssignableFrom(requiredType)) {
return Optional.of((T) new Properties());
} else if (Map.class.isAssignableFrom(requiredType)) {
return Optional.of((T) Collections.emptyMap());
}
return Optional.empty();
}
/**
* Returns a combined Map of all properties in the catalog.
*
* @param keyConvention The map key convention
* @param transformation The map format
* @return Map of all properties
*/
public Map getAllProperties(StringConvention keyConvention, MapFormat.MapTransformation transformation) {
Map map = new HashMap<>();
boolean isNested = transformation == MapFormat.MapTransformation.NESTED;
Arrays
.stream(getCatalog(keyConvention == StringConvention.RAW ? PropertyCatalog.RAW : PropertyCatalog.GENERATED))
.filter(Objects::nonNull)
.map(Map::entrySet)
.flatMap(Collection::stream)
.forEach((Map.Entry entry) -> {
String k = keyConvention.format(entry.getKey());
Object value = resolvePlaceHoldersIfNecessary(entry.getValue());
Map finalMap = map;
int index = k.indexOf('.');
if (index != -1 && isNested) {
String[] keys = DOT_PATTERN.split(k);
for (int i = 0; i < keys.length - 1; i++) {
if (!finalMap.containsKey(keys[i])) {
finalMap.put(keys[i], new HashMap<>());
}
Object next = finalMap.get(keys[i]);
if (next instanceof Map theMap) {
finalMap = theMap;
}
}
finalMap.put(keys[keys.length - 1], value);
} else {
finalMap.put(k, value);
}
});
return map;
}
/**
* @param name The property name
* @param entries The entries
* @param conversionContext The conversion context
* @return The subproperties
*/
protected Properties resolveSubProperties(String name, Map entries, ArgumentConversionContext> conversionContext) {
// special handling for maps for resolving sub keys
Properties properties = new Properties();
AnnotationMetadata annotationMetadata = conversionContext.getAnnotationMetadata();
StringConvention keyConvention = annotationMetadata.enumValue(MapFormat.class, "keyFormat", StringConvention.class)
.orElse(null);
if (keyConvention == StringConvention.RAW) {
entries = resolveEntriesForKey(name, false, PropertyCatalog.RAW);
}
String prefix = name + '.';
entries.entrySet().stream()
.filter(map -> map.getKey().startsWith(prefix))
.forEach(entry -> {
Object value = entry.getValue();
if (value != null) {
String key = entry.getKey().substring(prefix.length());
key = keyConvention != null ? keyConvention.format(key) : key;
properties.put(key, resolvePlaceHoldersIfNecessary(value.toString()));
}
});
return properties;
}
/**
* @param name The property name
* @param entries The entries
* @param conversionContext The conversion context
* @return The submap
*/
protected Map resolveSubMap(String name, Map entries, ArgumentConversionContext> conversionContext) {
// special handling for maps for resolving sub keys
AnnotationMetadata annotationMetadata = conversionContext.getAnnotationMetadata();
StringConvention keyConvention = annotationMetadata.enumValue(MapFormat.class, "keyFormat", StringConvention.class).orElse(null);
if (keyConvention == StringConvention.RAW) {
entries = resolveEntriesForKey(name, false, PropertyCatalog.RAW);
}
MapFormat.MapTransformation transformation = annotationMetadata.enumValue(
MapFormat.class,
"transformation",
MapFormat.MapTransformation.class)
.orElse(MapFormat.MapTransformation.NESTED);
return resolveSubMap(name, entries, conversionContext, keyConvention, transformation);
}
/**
* Resolves a submap for the given name and parameters.
*
* @param name The name
* @param entries The entries
* @param conversionContext The conversion context
* @param keyConvention The key convention to use
* @param transformation The map transformation to apply
* @return The resulting map
*/
@NonNull
protected Map resolveSubMap(
String name,
Map entries,
ArgumentConversionContext> conversionContext,
@Nullable StringConvention keyConvention,
MapFormat.MapTransformation transformation) {
final Argument> valueType = conversionContext.getTypeVariable("V").orElse(Argument.OBJECT_ARGUMENT);
boolean valueTypeIsList = List.class.isAssignableFrom(valueType.getType());
Map subMap = CollectionUtils.newLinkedHashMap(entries.size());
String prefix = name + '.';
for (Map.Entry entry : entries.entrySet()) {
final String key = entry.getKey();
if (valueTypeIsList && key.contains("[") && key.endsWith("]")) {
continue;
}
if (key.startsWith(prefix)) {
String subMapKey = key.substring(prefix.length());
Object value = resolvePlaceHoldersIfNecessary(entry.getValue());
if (transformation == MapFormat.MapTransformation.FLAT) {
subMapKey = keyConvention != null ? keyConvention.format(subMapKey) : subMapKey;
value = conversionService.convert(value, valueType).orElse(null);
subMap.put(subMapKey, value);
} else {
processSubmapKey(
subMap,
subMapKey,
value,
keyConvention
);
}
}
}
return subMap;
}
/**
* @param properties The property source
* @param convention The property convention
*/
@SuppressWarnings("MagicNumber")
protected void processPropertySource(PropertySource properties, PropertySource.PropertyConvention convention) {
this.propertySources.put(properties.getName(), properties);
synchronized (catalog) {
for (String property : properties) {
log.trace("Processing property key {}", property);
Object value = properties.get(property);
List resolvedProperties = resolvePropertiesForConvention(property, convention);
boolean first = true;
for (String resolvedProperty : resolvedProperties) {
int i = resolvedProperty.indexOf('[');
if (i > -1) {
String propertyName = resolvedProperty.substring(0, i);
Map entries = resolveEntriesForKey(propertyName, true, PropertyCatalog.GENERATED);
if (entries != null) {
entries.put(resolvedProperty, value);
expandProperty(resolvedProperty.substring(i), val -> entries.put(propertyName, val), () -> entries.get(propertyName), value);
}
if (first) {
Map normalized = resolveEntriesForKey(resolvedProperty, true, PropertyCatalog.NORMALIZED);
if (normalized != null) {
normalized.put(propertyName, value);
}
first = false;
}
} else {
Map entries = resolveEntriesForKey(resolvedProperty, true, PropertyCatalog.GENERATED);
if (entries != null) {
if (value instanceof List || value instanceof Map) {
collapseProperty(resolvedProperty, entries, value);
}
entries.put(resolvedProperty, value);
}
if (first) {
Map normalized = resolveEntriesForKey(resolvedProperty, true, PropertyCatalog.NORMALIZED);
if (normalized != null) {
normalized.put(resolvedProperty, value);
}
first = false;
}
}
}
final Map rawEntries = resolveEntriesForKey(property, true, PropertyCatalog.RAW);
if (rawEntries != null) {
rawEntries.put(property, value);
}
}
}
}
private void expandProperty(String property, Consumer
© 2015 - 2025 Weber Informatics LLC | Privacy Policy