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.context.exceptions.ConfigurationException;
import io.micronaut.core.annotation.AnnotationMetadata;
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.io.socket.SocketUtils;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.naming.conventions.StringConvention;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.value.MapPropertyResolver;
import io.micronaut.core.value.PropertyResolver;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import java.util.*;
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;
import java.util.stream.Collectors;
/**
* A {@link PropertyResolver} that resolves from one or many {@link PropertySource} instances.
*
* @author Graeme Rocher
* @since 1.0
*/
public class PropertySourcePropertyResolver implements PropertyResolver {
private static final Logger LOG = ClassUtils.getLogger(PropertySourcePropertyResolver.class);
private static final Pattern DOT_PATTERN = Pattern.compile("\\.");
private static final Pattern RANDOM_PATTERN = Pattern.compile("\\$\\{\\s?random\\.(\\S+?)\\}");
private static final char[] DOT_DASH = new char[] {'.', '-'};
private static final Object NO_VALUE = new Object();
private static final PropertyCatalog[] CONVENTIONS = {PropertyCatalog.GENERATED, PropertyCatalog.RAW};
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];
private final Random random = new Random();
private final Map containsCache = new ConcurrentHashMap<>(20);
private final Map resolvedValueCache = new ConcurrentHashMap<>(20);
/**
* Creates a new, initially empty, {@link PropertySourcePropertyResolver} for the given {@link ConversionService}.
*
* @param conversionService The {@link ConversionService}
*/
public PropertySourcePropertyResolver(ConversionService> conversionService) {
this.conversionService = conversionService;
this.propertyPlaceholderResolver = new DefaultPropertyPlaceholderResolver(this, conversionService);
}
/**
* 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;
} else {
Boolean result = containsCache.get(name);
if (result == null) {
for (PropertyCatalog convention : CONVENTIONS) {
Map entries = resolveEntriesForKey(name, false, convention);
if (entries != null) {
String finalName = trimIndex(name);
if (entries.containsKey(finalName)) {
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)) {
for (PropertyCatalog propertyCatalog : CONVENTIONS) {
Map entries = resolveEntriesForKey(name, false, propertyCatalog);
if (entries != null) {
String trimmedName = trimIndex(name);
if (entries.containsKey(trimmedName)) {
return true;
} else {
String finalName = trimmedName + ".";
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)) {
Map entries = resolveEntriesForKey(
name, false, PropertyCatalog.NORMALIZED);
if (entries != null) {
String prefix = name + '.';
return entries.keySet().stream().filter(k -> k.startsWith(prefix))
.map(k -> {
String withoutPrefix = k.substring(prefix.length());
int i = withoutPrefix.indexOf('.');
if (i > -1) {
return withoutPrefix.substring(0, i);
}
return withoutPrefix;
})
.collect(Collectors.toSet());
}
}
return Collections.emptySet();
}
@Override
public @NonNull Map getProperties(String name, StringConvention keyFormat) {
if (!StringUtils.isEmpty(name)) {
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
);
}
}
return Collections.emptyMap();
}
@Override
public Optional getProperty(@NonNull String name, @NonNull ArgumentConversionContext conversionContext) {
if (StringUtils.isEmpty(name)) {
return Optional.empty();
} else {
Objects.requireNonNull(conversionContext, "Conversion context should not be null");
Class requiredType = conversionContext.getArgument().getType();
boolean cacheableType = ClassUtils.isJavaLangType(requiredType);
Object cached = cacheableType ? resolvedValueCache.get(cacheKey(name, requiredType)) : null;
if (cached != null) {
return cached == NO_VALUE ? Optional.empty() : Optional.of((T) cached);
} else {
Map entries = resolveEntriesForKey(name, false, PropertyCatalog.GENERATED);
if (entries == null) {
entries = resolveEntriesForKey(name, false, PropertyCatalog.RAW);
}
if (entries != null) {
Object 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 (value != null) {
if (StringUtils.isNotEmpty(index)) {
if (value instanceof List) {
try {
value = ((List) value).get(Integer.valueOf(index));
} catch (NumberFormatException e) {
// ignore
}
} else if (value instanceof Map) {
try {
value = ((Map) value).get(index);
} catch (NumberFormatException e) {
// ignore
}
}
}
} else {
if (StringUtils.isNotEmpty(index)) {
String subKey = newKey + '.' + index;
value = entries.get(subKey);
}
}
}
}
if (value != null) {
Optional converted;
value = resolvePlaceHoldersIfNecessary(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(name, requiredType), converted.orElse((T) NO_VALUE));
}
return converted;
} else if (cacheableType) {
resolvedValueCache.put(cacheKey(name, requiredType), 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, 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));
}
}
}
}
if (LOG.isTraceEnabled()) {
LOG.trace("No value found for property: {}", name);
}
Class requiredType = conversionContext.getArgument().getType();
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();
}
@NotNull
private String cacheKey(@NonNull String name, Class requiredType) {
return name + '|' + requiredType.getSimpleName();
}
/**
* 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(catalog)
.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) {
finalMap = ((Map) next);
}
}
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 = new LinkedHashMap<>(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) {
if (LOG.isTraceEnabled()) {
LOG.trace("Processing property key {}", property);
}
Object value = properties.get(property);
if (value instanceof CharSequence) {
value = processRandomExpressions(convention, property, (CharSequence) value);
} else if (value instanceof List) {
final ListIterator i = ((List) value).listIterator();
while (i.hasNext()) {
final Object o = i.next();
if (o instanceof CharSequence) {
final CharSequence newValue = processRandomExpressions(convention, property, (CharSequence) o);
if (newValue != o) {
i.set(newValue);
}
}
}
}
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