io.helidon.config.hocon.mp.HoconMpConfigSource Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of helidon-config-hocon-mp Show documentation
Show all versions of helidon-config-hocon-mp Show documentation
HOCON support for Helidon MP.
The newest version!
/*
* Copyright (c) 2022, 2024 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.hocon.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.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 com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
import org.eclipse.microprofile.config.spi.ConfigSource;
/**
* MicroProfile {@link org.eclipse.microprofile.config.spi.ConfigSource} that can be used
* to add HOCON/JSON files from classpath or file system using the
* {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver#getBuilder()}.
* The HOCON/JSON 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 HoconMpConfigSource implements ConfigSource {
private final String name;
private Map properties;
private static final boolean RESOLVING_ENABLED = true;
private static final ConfigResolveOptions RESOLVE_OPTIONS = ConfigResolveOptions.defaults();
private HoconMpConfigSource(String name, Map properties) {
this.properties = properties;
this.name = "hocon: " + name;
}
/**
* Load a HOCON/JSON config source from file system.
*
* @param path path to the HOCON/JSON 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)) {
ConfigParseOptions parseOptions = getConfigParseOptions(path);
return create(name, reader, parseOptions);
} catch (IOException e) {
throw new ConfigException("Failed to load HOCON/JSON config source from path: " + path.toAbsolutePath(), e);
}
}
/**
* Load a HOCON/JSON config source from a reader.
*
* @param name the name of the config
* @param reader that will read the configuration
* @param parseOptions of the content
* @return config source loaded
*/
public static ConfigSource create(String name, Reader reader, ConfigParseOptions parseOptions) {
Config typesafeConfig = toConfig(reader, parseOptions);
// this is a mutable HashMap that we can use
Map props =
fromConfig(typesafeConfig.root().isEmpty() ? ConfigFactory.empty().root() : typesafeConfig.root());
return new HoconMpConfigSource(name, props);
}
/**
* Load a HOCON/JSON config source from URL.
* The URL may be any URL which is supported 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)) {
String name = url.toString();
ConfigParseOptions parseOptions = getConfigParseOptions(url);
Config typesafeConfig = toConfig(reader, parseOptions);
if (typesafeConfig.root().isEmpty()) { // empty source
return new HoconMpConfigSource(name, Map.of());
}
return new HoconMpConfigSource(name, fromConfig(typesafeConfig.root()));
} catch (Exception e) {
throw new ConfigException("Failed to configure HOCON/JSON config source", e);
}
}
private static ConfigParseOptions getConfigParseOptions(URL url) {
HoconMpConfigIncluder includer = new HoconMpConfigIncluder();
ConfigParseOptions parseOptions = ConfigParseOptions.defaults().appendIncluder(includer);
includer.parseOptions(parseOptions);
includer.relativeUrl(getParentResourcePath(url.toString()));
includer.charset(StandardCharsets.UTF_8);
return parseOptions;
}
private static ConfigParseOptions getConfigParseOptions(Path path) {
HoconMpConfigIncluder includer = new HoconMpConfigIncluder();
ConfigParseOptions parseOptions = ConfigParseOptions.defaults().appendIncluder(includer);
includer.parseOptions(parseOptions);
includer.relativePath(path.getParent());
includer.charset(StandardCharsets.UTF_8);
return parseOptions;
}
private static Config toConfig(Reader content, ConfigParseOptions parseOptions) {
Config typesafeConfig = ConfigFactory.parseReader(
content, parseOptions);
if (RESOLVING_ENABLED) {
typesafeConfig = typesafeConfig.resolve(RESOLVE_OPTIONS);
}
return typesafeConfig;
}
/**
* Create from HOCON/JSON 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 HOCON/JSON \"" + resource + "\" from classpath", e);
}
return sources;
}
/**
* Create from HOCON/JSON file(s) on classpath with profile support.
*
* @param resource resource name to locate on classpath (looks for all instances)
* @param profile name of the profile to use
* @return list of config sources discovered (may be zero length)
*/
public static List classPath(String resource, String profile) {
Objects.requireNonNull(profile, "Profile must be defined");
List sources = new LinkedList<>();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
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 HOCON/JSON \"" + 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 String getParentResourcePath(String resource) {
// this works the same on windows and Unix systems (classpath is always forward slashes)
int lastSlash = resource.lastIndexOf('/');
String rootOfResource;
if (lastSlash > -1) {
rootOfResource = resource.substring(0, lastSlash + 1);
} else {
rootOfResource = resource;
}
return rootOfResource;
}
private static Map fromConfig(ConfigObject config) {
Map result = new HashMap<>();
process(result, "", config);
return result;
}
private static void process(Map resultMap, String prefix, ConfigObject config) {
config.forEach((unescapedKey, value) -> {
String key = io.helidon.config.Config.Key.escapeName(unescapedKey);
processNext(resultMap, prefix(prefix, key), value);
});
}
private static void process(Map resultMap, String prefix, ConfigList configList) {
int counter = 0;
for (ConfigValue value : configList) {
processNext(resultMap, prefix(prefix, String.valueOf(counter)), value);
counter++;
}
}
private static void processNext(Map resultMap,
String prefix,
ConfigValue value) {
if (value instanceof ConfigList) {
process(resultMap, prefix, (ConfigList) value);
} else if (value instanceof ConfigObject) {
process(resultMap, prefix, (ConfigObject) value);
} else {
Object unwrapped = value.unwrapped();
String stringValue = (null == unwrapped) ? "" : String.valueOf(unwrapped);
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;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy