io.helidon.config.yaml.mp.YamlMpConfigSource Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of helidon-config-yaml-mp Show documentation
Show all versions of helidon-config-yaml-mp Show documentation
YAML support for Helidon MP.
The newest version!
/*
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.config.yaml.mp;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import io.helidon.config.ConfigException;
import io.helidon.config.MutabilitySupport;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
/**
* MicroProfile {@link org.eclipse.microprofile.config.spi.ConfigSource} that can be used
* to add YAML files from classpath or file system using the
* {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver#getBuilder()}.
* The YAML file is transformed to a flat map as follows:
* Object nodes
*
* Each node in the tree is dot separated.
*
* server:
* host: "localhost"
* port: 8080
*
* Will be transformed to the following properties:
*
* server.host=localhost
* server.port=8080
*
* List nodes (arrays)
*
* Each node will be indexed (0 based)
*
* providers:
* - abac:
* enabled: true
* names: ["first", "second", "third"]
*
* Will be transformed to the following properties:
*
* providers.0.abac.enabled=true
* names.0=first
* names.1=second
* names.2=third
*
*/
@SuppressWarnings("rawtypes")
public class YamlMpConfigSource implements ConfigSource {
private final Map properties;
private final String name;
private YamlMpConfigSource(String name, Map properties) {
this.properties = properties;
this.name = "yaml: " + name;
}
/**
* Load a YAML config source from file system.
*
* @param path path to the YAML file
* @return config source loaded from the file
* @see #create(java.net.URL)
*/
public static ConfigSource create(Path path) {
String name = path.toAbsolutePath().toString();
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
Map yamlMap = toMap(reader);
// this is a mutable HashMap that we can use
Map props = fromMap(yamlMap == null ? Map.of() : yamlMap);
if ("true".equals(props.get("helidon.config.polling.enabled"))) {
String durationString = props.get("helidon.config.polling.duration");
Duration duration;
if (durationString == null) {
duration = Duration.ofSeconds(10);
} else {
duration = Duration.parse(durationString);
}
MutabilitySupport.poll(path, duration, changed -> update(path, props), changed -> props.clear());
} else if ("true".equals(props.get("helidon.config.watcher.enabled"))) {
MutabilitySupport.watch(path, changed -> update(path, props), changed -> props.clear());
}
return new YamlMpConfigSource(name, props);
} catch (IOException e) {
throw new ConfigException("Failed to load YAML config source from path: " + path.toAbsolutePath(), e);
}
}
private static void update(Path path, Map originalProps) {
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
Map yamlMap = toMap(reader);
// this is a mutable HashMap that we can use
Map props = fromMap(yamlMap == null ? Map.of() : yamlMap);
// first delete those that no longer exist
originalProps.keySet().removeIf(it -> !props.containsKey(it));
originalProps.putAll(props);
} catch (IOException e) {
throw new ConfigException("Failed to load updated YAML config source from path: " + path.toAbsolutePath(), e);
}
}
/**
* Load a YAML config source from URL.
* The URL may be any URL which is support by the used JVM.
*
* @param url url of the resource
* @return config source loaded from the URL
*/
public static ConfigSource create(URL url) {
try (InputStreamReader reader = new InputStreamReader(url.openConnection().getInputStream(), StandardCharsets.UTF_8)) {
return create(url.toString(), reader);
} catch (Exception e) {
throw new ConfigException("Failed to configure YAML config source", e);
}
}
/**
* Create from YAML content as a reader.
* This method will NOT close the reader.
*
* @param name name of the config source
* @param content reader with the YAML content
* @return config source loaded from the content
*/
public static ConfigSource create(String name, Reader content) {
Map yamlMap = toMap(content);
if (yamlMap == null) { // empty source
return new YamlMpConfigSource(name, Map.of());
}
return new YamlMpConfigSource(name, fromMap(yamlMap));
}
/**
* Create from YAML file(s) on classpath.
*
* @param resource resource name to locate on classpath (looks for all instances)
* @return list of config sources discovered (may be zero length)
*/
public static List classPath(String resource) {
List sources = new LinkedList<>();
try {
Thread.currentThread().getContextClassLoader().getResources(resource)
.asIterator()
.forEachRemaining(it -> sources.add(create(it)));
} catch (IOException e) {
throw new IllegalStateException("Failed to read YAML \"" + resource + "\" from classpath", e);
}
return sources;
}
/**
* Create from YAML file(s) on classpath with profile support.
*
* @param resource resource name to locate on classpath (looks for all instances)
* @param profile configuration profile to use, must not be null
* @return list of config sources discovered (may be zero length)
*/
public static List classPath(String resource, String profile) {
return classPathConfigSources(resource, profile, Thread.currentThread().getContextClassLoader());
}
/**
* Create from YAML file(s) on classpath from specified classloader with profile support.
*
* @param resource resource name to locate on classpath (looks for all instances)
* @param profile configuration profile to use, must not be null
* @param classLoader classloader where resource will be retrieved from
* @return list of config sources discovered (may be zero length)
*/
public static List classPath(String resource, String profile, ClassLoader classLoader) {
Objects.requireNonNull(profile, "Profile must be defined");
Objects.requireNonNull(classLoader, "ClassLoader must be defined");
return classPathConfigSources(resource, profile, classLoader);
}
private static List classPathConfigSources(String resource, String profile, ClassLoader classLoader) {
List sources = new LinkedList<>();
try {
Enumeration baseResources = classLoader.getResources(resource);
Enumeration profileResources = classLoader.getResources(toProfileResource(resource, profile));
if (profileResources.hasMoreElements()) {
List profileResourceList = new LinkedList<>();
profileResources.asIterator()
.forEachRemaining(profileResourceList::add);
baseResources.asIterator()
.forEachRemaining(it -> {
String pathBase = pathBase(it.toString());
// we need to find profile that belongs to this
for (URL url : profileResourceList) {
String profilePathBase = pathBase(url.toString());
if (pathBase.equals(profilePathBase)) {
// Main is the profile config file and fallback is the original config file
sources.add(create(create(url), create(it)));
} else {
sources.add(create(it));
}
}
});
} else {
baseResources
.asIterator()
.forEachRemaining(it -> sources.add(create(it)));
}
} catch (IOException e) {
throw new IllegalStateException("Failed to read YAML \"" + resource + "\" from classpath", e);
}
return sources;
}
private static ConfigSource create(ConfigSource main, ConfigSource fallback) {
String name = main.getName() + " (" + fallback.getName() + ")";
return new ConfigSource() {
@Override
public Set getPropertyNames() {
Set result = new HashSet<>(fallback.getPropertyNames());
result.addAll(main.getPropertyNames());
return result;
}
@Override
public String getValue(String propertyName) {
String value = main.getValue(propertyName);
if (value == null) {
return fallback.getValue(propertyName);
}
return value;
}
@Override
public String getName() {
return name;
}
@Override
public Map getProperties() {
Map result = new HashMap<>(fallback.getProperties());
result.putAll(main.getProperties());
return result;
}
};
}
private static Map fromMap(Map yamlMap) {
Map result = new HashMap<>();
process(result, "", yamlMap);
return result;
}
private static void process(Map resultMap, String prefix, Map yamlMap) {
yamlMap.forEach((key, value) -> {
processNext(resultMap, prefix(prefix, key.toString()), value);
});
}
private static void process(Map resultMap, String prefix, List yamlList) {
int counter = 0;
for (Object value : yamlList) {
processNext(resultMap, prefix(prefix, String.valueOf(counter)), value);
counter++;
}
}
private static void processNext(Map resultMap,
String prefix,
Object value) {
if (value instanceof List) {
process(resultMap, prefix, (List) value);
} else if (value instanceof Map) {
process(resultMap, prefix, (Map) value);
} else {
String stringValue = (null == value) ? "" : value.toString();
resultMap.put(prefix, stringValue);
}
}
private static String prefix(String prefix, String stringKey) {
if (prefix.isEmpty()) {
return stringKey;
}
return prefix + "." + stringKey;
}
private static String pathBase(String path) {
int i = path.lastIndexOf('/');
int y = path.lastIndexOf('!');
int z = path.lastIndexOf(':');
int b = path.lastIndexOf('\\');
// we need the last index before the file name - so the highest number of all of the above
int max = Math.max(i, y);
max = Math.max(max, z);
max = Math.max(max, b);
if (max > -1) {
return path.substring(0, max);
}
return path;
}
private static String toProfileResource(String resource, String profile) {
int i = resource.lastIndexOf('.');
if (i > -1) {
return resource.substring(0, i) + "-" + profile + resource.substring(i);
}
return resource + "-" + profile;
}
@Override
public Set getPropertyNames() {
return Collections.unmodifiableSet(properties.keySet());
}
@Override
public Map getProperties() {
return Collections.unmodifiableMap(properties);
}
@Override
public String getValue(String propertyName) {
return properties.get(propertyName);
}
@Override
public String getName() {
return name;
}
static Map toMap(Reader reader) {
// the default of Snake YAML is a Map, safe constructor makes sure we never deserialize into anything
// harmful
Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
return (Map) yaml.loadAs(reader, Object.class);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy