All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.helidon.config.mp.MpConfigSources Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2020, 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.mp;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
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.Properties;
import java.util.ServiceLoader;
import java.util.Set;

import io.helidon.common.HelidonServiceLoader;
import io.helidon.config.Config;
import io.helidon.config.ConfigException;
import io.helidon.config.MutabilitySupport;
import io.helidon.config.mp.spi.MpMetaConfigProvider;

import org.eclipse.microprofile.config.spi.ConfigSource;

/**
 * Utilities for MicroProfile Config {@link org.eclipse.microprofile.config.spi.ConfigSource}.
 * 

* The following methods create MicroProfile config sources to help with manual setup of Config * from {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver#getBuilder()}: *

    *
  • {@link #systemProperties()} - system properties config source
  • *
  • {@link #environmentVariables()} - environment variables config source
  • *
  • {@link #create(java.nio.file.Path)} - load a properties file from file system
  • *
  • {@link #create(String, java.nio.file.Path)} - load a properties file from file system with custom name
  • *
  • {@link #create(java.util.Map)} - create an in-memory source from map
  • *
  • {@link #create(String, java.util.Map)} - create an in-memory source from map with custom name
  • *
  • {@link #create(java.util.Properties)} - create an in-memory source from properties
  • *
  • {@link #create(String, java.util.Properties)} - create an in-memory source from properties with custom name
  • *
* The following methods add integration with Helidon SE Config: *
    *
  • {@link #create(io.helidon.config.spi.ConfigSource)} - create a MicroProfile config source from Helidon SE config * source
  • *
  • {@link #create(io.helidon.config.Config)} - create a MicroProfile config source from Helidon SE Config instance
  • *
*/ public final class MpConfigSources { static final Map MP_META_PROVIDERS; static { List mpMetaConfigProviders = HelidonServiceLoader.builder(ServiceLoader.load(MpMetaConfigProvider.class)) .addService(new MpEnvironmentVariablesMetaConfigProvider()) .addService(new MpSystemPropertiesMetaConfigProvider()) .addService(new MpPropertiesMetaConfigProvider()) .build() .asList(); // Helidon service loader uses Weight and Weighted by default, we want to use priorities here Priorities.sort(mpMetaConfigProviders, Prioritized.DEFAULT_PRIORITY); Map theMap = new HashMap<>(); // ordered by priority for (MpMetaConfigProvider mpMetaConfigProvider : mpMetaConfigProviders) { for (String supportedType : mpMetaConfigProvider.supportedTypes()) { theMap.putIfAbsent(supportedType, mpMetaConfigProvider); } } MP_META_PROVIDERS = Map.copyOf(theMap); } private MpConfigSources() { } /** * In memory config source based on the provided map. * The config source queries the map each time {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} * is called. * * @param name name of the source * @param theMap map serving as configuration data * @return a new config source */ public static ConfigSource create(String name, Map theMap) { return new MpMapSource(name, theMap); } /** * In memory config source based on the provided map. * The config source queries the map each time {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} * is called. * * @param theMap map serving as configuration data * @return a new config source */ public static ConfigSource create(Map theMap) { return create("Map", theMap); } /** * {@link java.util.Properties} config source based on a file on file system. * The file is read just once, when the source is created and further changes to the underlying file are * ignored. * * @param path path of the properties file on the file system * @return a new config source */ public static ConfigSource create(Path path) { return create(path.toString(), path); } /** * {@link java.util.Properties} config source based on a URL. * The URL is read just once, when the source is created and further changes to the underlying resource are * ignored. * * @param url url of the properties file (any URL scheme supported by JVM can be used) * @return a new config source */ public static ConfigSource create(URL url) { String name = url.toString(); try { URLConnection urlConnection = url.openConnection(); try (InputStream inputStream = urlConnection.getInputStream()) { Properties properties = new Properties(); properties.load(inputStream); return create(name, properties); } } catch (Exception e) { throw new ConfigException("Failed to load ", e); } } /** * {@link java.util.Properties} config source based on a URL with a profile override. * The URL is read just once, when the source is created and further changes to the underlying resource are * ignored. * * @param url url of the properties file (any URL scheme supported by JVM can be used) * @param profileUrl url of the properties file of profile specific configuration * @return a new config source */ public static ConfigSource create(URL url, URL profileUrl) { ConfigSource defaultSource = create(url); ConfigSource profileSource = create(profileUrl); return composite(profileSource, defaultSource); } /** * {@link java.util.Properties} config source based on a file on file system. * The file is read just once, when the source is created and further changes to the underlying file are * ignored. * * @param name name of the config source * @param path path of the properties file on the file system * @return a new config source */ public static ConfigSource create(String name, Path path) { Properties props = new Properties(); try (InputStream in = Files.newInputStream(path)) { props.load(in); } catch (IOException e) { throw new ConfigException("Failed to read properties from " + path.toAbsolutePath()); } if ("true".equals(props.getProperty("helidon.config.polling.enabled"))) { String durationString = props.getProperty("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.getProperty("helidon.config.watcher.enabled"))) { MutabilitySupport.watch(path, changed -> update(path, props), changed -> props.clear()); } return create(name, props); } /** * In memory config source based on the provided properties. * The config source queries the properties each time * {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} * is called. * * @param properties serving as configuration data * @return a new config source */ public static ConfigSource create(Properties properties) { return create("Properties", properties); } /** * In memory config source based on the provided properties. * The config source queries the properties each time * {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} * is called. * * @param name name of the config source * @param properties serving as configuration data * @return a new config source */ @SuppressWarnings("rawtypes") public static ConfigSource create(String name, Properties properties) { Map map = properties; return new MpMapSource(name, map); } /** * Environment variables config source. * This source takes care of replacement of properties by environment variables as defined * in MicroProfile Config specification. * This config source is immutable and caching. * * @return a new config source */ public static ConfigSource environmentVariables() { return new MpEnvironmentVariablesSource(); } /** * In memory config source based on system properties. * The config source queries the properties each time * {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} * is called. * * @return a new config source */ public static ConfigSource systemProperties() { return new MpSystemPropertiesSource(); } /** * Find all resources on classpath and return a config source for each. * Order is kept as provided by class loader. * * @param resource resource to find * @return a config source for each resource on classpath, empty if none found */ public static List classPath(String resource) { return classPath(Thread.currentThread().getContextClassLoader(), resource); } /** * Find all resources on classpath and return a config source for each. * Order is kept as provided by class loader. * * The profile will be used to locate a source with {@code -${profile}} name, such as * {@code microprofile-config-dev.properties} for dev profile. * * @param resource resource to find * @param profile configuration profile to use, must not be null * @return a config source for each resource on classpath, empty if none found */ public static List classPath(String resource, String profile) { return classPath(Thread.currentThread().getContextClassLoader(), resource, profile); } /** * Find all resources on classpath and return a config source for each. * Order is kept as provided by class loader. * * @param classLoader class loader to use to locate the resources * @param resource resource to find * @return a config source for each resource on classpath, empty if none found */ public static List classPath(ClassLoader classLoader, String resource) { List sources = new LinkedList<>(); try { classLoader.getResources(resource) .asIterator() .forEachRemaining(it -> sources.add(create(it))); } catch (IOException e) { throw new IllegalStateException("Failed to read \"" + resource + "\" from classpath", e); } return sources; } /** * Find all resources on classpath and return a config source for each with a profile. * Order is kept as provided by class loader. * * The profile will be used to locate a source with {@code -${profile}} name, such as * {@code microprofile-config-dev.properties} for dev profile. * * @param classLoader class loader to use to locate the resources * @param resource resource to find * @param profile configuration profile to use, must not be null * @return a config source for each resource on classpath, empty if none found */ public static List classPath(ClassLoader classLoader, String resource, String profile) { Objects.requireNonNull(profile, "Profile must be defined"); 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)) { sources.add(create(it, url)); } else { sources.add(create(it)); } } }); } else { baseResources .asIterator() .forEachRemaining(it -> sources.add(create(it))); } } catch (IOException e) { throw new IllegalStateException("Failed to read \"" + resource + "\" from classpath", e); } return sources; } /** * Config source based on a Helidon SE config source. * This is to support Helidon SE features in Helidon MP. * * The config source will be immutable regardless of configured polling strategy or change watchers. * * @param helidonConfigSource config source to use * @return a new MicroProfile Config config source */ public static ConfigSource create(io.helidon.config.spi.ConfigSource helidonConfigSource) { return MpHelidonSource.create(helidonConfigSource); } /** * Config source base on a Helidon SE config instance. * This is to support advanced Helidon SE features in Helidon MP. * * The config source will be mutable if the config uses polling strategy and/or change watchers. * Each time the {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} is called, * the latest config version will be queried. * * @param config Helidon SE configuration * @return a new MicroProfile Config config source */ public static ConfigSource create(Config config) { return new MpHelidonConfigSource(Objects.requireNonNull(config, "Config cannot be null")); } /** * Config source base on a Reader instance for the given type. * This method will not close the reader. * * @param type of the given reader (properties, YAML, etc). * @param reader with the configuration. * @return a new MicroProfile config source */ public static ConfigSource create(String type, Reader reader) { Objects.requireNonNull(type, "Type cannot be null"); Objects.requireNonNull(reader, "Reader cannot be null"); MpMetaConfigProvider provider = MP_META_PROVIDERS.get(type); if (provider == null) { throw new IllegalArgumentException("There is no MpMetaConfigProvider registered for type " + type + ". Make sure the desired provider exists in modulepath/classpath."); } return provider.create(reader); } /** * Config source base on a URL for the given type. * This method will not close the reader. * * @param type of the given URL (properties, YAML, etc). * @param url with resource. * @return a new MicroProfile config source */ public static ConfigSource create(String type, URL url) { Objects.requireNonNull(type, "Type cannot be null"); Objects.requireNonNull(url, "URL cannot be null"); MpMetaConfigProvider provider = MP_META_PROVIDERS.get(type); if (provider == null) { throw new IllegalArgumentException("There is no MpMetaConfigProvider registered for type " + type + " in " + url + ". Make sure the desired provider exists in modulepath/classpath."); } try (InputStreamReader reader = new InputStreamReader(url.openConnection().getInputStream(), StandardCharsets.UTF_8)) { return provider.create(reader); } catch (Exception e) { throw new ConfigException("Failed to configure " + type + " config source", e); } } /** * Create a composite config source that uses the main first, and if it does not find * a property in main, uses fallback. This is useful to set up a config source with a profile, * where the profile source is {@code main} and the non-profile source is {@code fallback}. * * @param main look for properties here first * @param fallback if not found in main, look here * @return a new config source */ static ConfigSource composite(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 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; } private static void update(Path path, Properties originalProperties) { Properties props = new Properties(); try (InputStream in = Files.newInputStream(path)) { props.load(in); } catch (IOException e) { throw new ConfigException("Failed to read properties from " + path.toAbsolutePath()); } originalProperties.keySet().removeIf(it -> !props.containsKey(it)); originalProperties.putAll(props); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy