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

ph.extremelogic.common.core.config.ConfigurationLoader Maven / Gradle / Ivy

/*
 * 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 ph.extremelogic.common.core.config;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.yaml.snakeyaml.Yaml;
import ph.extremelogic.common.core.config.encrypt.DefaultPropertyEncryptor;
import ph.extremelogic.common.core.config.encrypt.PropertyEncryptor;

import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.*;
import java.util.regex.Pattern;

import static ph.extremelogic.common.core.config.util.FileUtil.getInputStream;
import static ph.extremelogic.common.core.config.util.ReflectiveValueSetter.setFieldValue;

/**
 * The ConfigurationLoader class is responsible for loading configuration properties
 * from various sources such as YAML files, environment variables, and system properties.
 * It also supports placeholder resolution and property encryption.
 * 

* This class provides methods to set custom configuration names, property encryptors, * and behavior for handling exceptions during configuration loading. *

*/ public class ConfigurationLoader { private static final Log logger = LogFactory.getLog(ConfigurationLoader.class); public static final String DEFAULT_CONFIG_NAME = "config"; public static final String ENCRYPTION_KEY_PROP = DEFAULT_CONFIG_NAME + ".encryption.key"; public static final String ENCRYPTION_KEY_ENV = DEFAULT_CONFIG_NAME.toUpperCase() + "_ENCRYPTION_KEY"; public static final String CONFIG_PROFILES_ACTIVE_PROP = DEFAULT_CONFIG_NAME + ".profiles.active"; public static final String CONFIG_PROFILES_ACTIVE_ENV = DEFAULT_CONFIG_NAME.toUpperCase() + "_PROFILES_ACTIVE"; private final Map configuration = new LinkedHashMap<>(); private Map env = System.getenv(); private static final Pattern NON_ALPHA_NUMERIC = Pattern.compile("[^a-zA-Z0-9]"); private PropertyEncryptor propertyEncryptor; private List activeProfiles = new ArrayList<>(); private String configName = DEFAULT_CONFIG_NAME; // Need this outside method because it slows down the method private final Pattern patternEnc = Pattern.compile("ENC\\((.*)\\)"); private final Pattern patternVar = Pattern.compile("\\$\\{(.+?)\\}"); private boolean throwException = true; /** * Annotation to mark fields for configuration value injection. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Value { String value(); } protected void put(String key, String value) { configuration.put(normalizeKey(key), value); } private String normalizeKey(String key) { // Convert to lowercase and remove all non-alphanumeric characters return NON_ALPHA_NUMERIC.matcher(key.toLowerCase()).replaceAll(""); } /** * Loads properties from a .properties file. * * @param fileName the name of the file to load */ public void loadProperties(String fileName) { var properties = new Properties(); try (var input = getInputStream(fileName)) { properties.load(input); for (var key : properties.stringPropertyNames()) { var value = properties.getProperty(key); put(key, decryptIfNeeded(value)); } } catch (ConfigurationException | IOException e) { var msg = "Unable to load " + fileName + " " + e.getLocalizedMessage(); logger.warn(msg); if (throwException) { throw new ConfigurationException(msg, e); } } } public void loadProperties() { loadProperties(configName + ".properties"); } /** * Loads configuration from a YAML file. * * @param fileName the name of the file to load */ public void loadYaml(String fileName) { Yaml yaml = new Yaml(); try (var input = getInputStream(fileName)) { Map yamlMap = yaml.load(input); flattenMap("", yamlMap); } catch (ConfigurationException | IOException e) { var msg = "Unable to load " + fileName + " " + e.getLocalizedMessage(); logger.warn(msg); if (throwException) { throw new ConfigurationException(msg, e); } } } public void loadYaml() { loadYaml(configName + ".yml"); } /** * Loads configuration from all supported sources (properties, YAML, environment variables). */ public void loadConfiguration(String[] args, boolean throwException) { var name = configName; this.throwException = throwException; setEncryptionKeyByPrecedence(args); // Load default configurations loadProperties(name + ".properties"); loadYaml(name + ".yml"); var activeProfile = getConfigProfilesActiveByPrecedence(args); // Load active profiles if (activeProfile != null) { activeProfiles = Arrays.asList(activeProfile.split(",")); for (var profile : activeProfiles) { var filename = name + "-" + profile.trim(); loadProperties(filename); loadYaml(filename); } // Properties and Yaml calls above had probably // overwritten the CONFIG_PROFILES_ACTIVE configuration.put(CONFIG_PROFILES_ACTIVE_PROP, activeProfile); } // Load environment variables (higher precedence) loadEnvironmentVariables(); // Load system properties (even higher precedence) loadSystemProperties(); // Load command line arguments (highest precedence) if (args != null && args.length > 0) { loadCommandLineArguments(args); } // Resolve property placeholders resolvePlaceholders(); } public void loadConfiguration() { loadConfiguration(null, false); } public void loadConfiguration(boolean throwException) { loadConfiguration(null, throwException); } private String getConfigProfilesActiveByPrecedence(String[] args) { String profile = null; // Load command line arguments (highest precedence) if (args != null && args.length > 0) { for (String arg : args) { if (arg.startsWith("--" + CONFIG_PROFILES_ACTIVE_PROP + "=")) { profile = arg.substring(("--" + CONFIG_PROFILES_ACTIVE_PROP + "=").length()); break; } } } // Load system properties if previous is null if (profile == null) { profile = System.getProperty(CONFIG_PROFILES_ACTIVE_PROP); } // Load env variable if previous is null if (profile == null) { profile = env.get(CONFIG_PROFILES_ACTIVE_ENV); } // Return default if all above are null if (profile == null || profile.isBlank()) { profile = configuration.get(CONFIG_PROFILES_ACTIVE_PROP); } return profile; } private void setEncryptionKeyByPrecedence(String[] args) { String encryptionKey = null; // Load command line arguments (highest precedence) if (args != null && args.length > 0) { for (String arg : args) { if (arg.startsWith("--" + ENCRYPTION_KEY_PROP + "=")) { encryptionKey = arg.substring(("--" + ENCRYPTION_KEY_PROP + "=").length()); break; } } } // Load system properties if previous is null if (encryptionKey == null) { encryptionKey = System.getProperty(ENCRYPTION_KEY_PROP); } // Load env variable if previous is null if (encryptionKey == null) { encryptionKey = env.get(ENCRYPTION_KEY_ENV); } if (null != encryptionKey) { configuration.put(ENCRYPTION_KEY_PROP, encryptionKey); } } private String getConfigValue(String propertyKey, String envKey, String defaultValue) { String value = getProperty(propertyKey); if (value == null || value.isEmpty()) { value = env.get(envKey); } return (value != null && !value.isEmpty()) ? value : defaultValue; } /** * Injects configuration values into fields annotated with {@link Value}. * * @param obj the object to inject configuration into * @throws IllegalAccessException if the fields cannot be accessed */ public void injectConfig(Object obj) throws IllegalAccessException { Class clazz = obj.getClass(); for (var field : clazz.getDeclaredFields()) { Value valueAnnotation = field.getAnnotation(Value.class); if (valueAnnotation != null) { var propertyWithDefaultValue = valueAnnotation.value(); var propertyTmp = propertyWithDefaultValue.replace("${", "").replace("}", "").trim(); var parts = propertyTmp.split(":", 2); var propertyKey = parts[0]; var defaultValue = parts.length > 1 ? parts[1] : null; var value = getConfigValue(propertyKey, propertyKey.toUpperCase().replace('.', '_'), defaultValue); if (value == null && defaultValue == null) { continue; } try { field.setAccessible(true); setFieldValue(field, obj, value); } catch (Exception e) { logger.error("Failed to set field value: " + field.getName(), e); } } } } /** * Recursively flattens a nested map and adds its entries to the configuration map. * * @param prefix the current prefix for the keys * @param map the map to flatten */ private void flattenMap(String prefix, Map map) { for (Map.Entry entry : map.entrySet()) { String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey(); if (entry.getValue() instanceof Map) { @SuppressWarnings("unchecked") Map nestedMap = (Map) entry.getValue(); flattenMap(key, nestedMap); } else { put(key, decryptIfNeeded(entry.getValue().toString())); } } } /** * Loads environment variable to override configuration. */ protected void loadEnvironmentVariables() { for (Map.Entry entry : env.entrySet()) { String key = convertEnvToPropertyKey(entry.getKey()); if (key != null) { put(key, decryptIfNeeded(entry.getValue())); } } } /** * Mock environment variables for testing purposes. * * @param envVars the mock environment variables */ protected void mockEnvironmentVariables(Map envVars) { env = envVars; } /** * Converts an environment variable key to a property key format. * * @param envKey the environment variable key * @return the converted property key, or null if the key is not valid */ private String convertEnvToPropertyKey(String envKey) { return envKey.toLowerCase().replace('_', '.'); } /** * Gets a configuration property value. * * @param key the key of the property to retrieve * @return the value of the property, or null if not found */ public String getProperty(String key) { return configuration.get(normalizeKey(key)); } /** * Decrypts the value if it's encrypted, otherwise returns the original value. * * @param value the value to decrypt if needed * @return the decrypted value or the original value if not encrypted */ private String decryptIfNeeded(String value) { var matcher = patternEnc.matcher(value); if (matcher.find()) { String encryptedValue = matcher.group(1); return decrypt(encryptedValue); } return value; } /** * Gets the encryption key from environment variables, system properties, or configuration. * * @return the encryption key * @throws IllegalStateException if the encryption key is not found */ private String getEncryptionKey() { var encKey = System.getenv(ENCRYPTION_KEY_ENV); if (null != encKey) { return encKey; } encKey = configuration.get(ENCRYPTION_KEY_PROP); if (null != encKey) { return encKey; } throw new IllegalStateException("Encryption key not found. Please set " + ENCRYPTION_KEY_PROP); } /** * Initializes the PropertyEncryptor with the encryption key. */ private void initializeEncryptor() { if (propertyEncryptor == null) { propertyEncryptor = new DefaultPropertyEncryptor(getEncryptionKey()); } } /** * Encrypts a value. * * @param value the value to encrypt * @return the encrypted value wrapped in ENC() */ public String encrypt(String value) { initializeEncryptor(); return "ENC(" + propertyEncryptor.encrypt(value) + ")"; } /** * Decrypts an encrypted value. * * @param encryptedValue the encrypted value to decrypt * @return the decrypted value */ private String decrypt(String encryptedValue) { initializeEncryptor(); return propertyEncryptor.decrypt(encryptedValue); } /** * Returns the list of active profiles for the current configuration. * * @return A list of strings representing the active profiles. */ public List getActiveProfiles() { return activeProfiles; } /** * Loads system properties into the configuration. * Any encrypted values are decrypted before being added. */ private void loadSystemProperties() { var sysProps = System.getProperties(); for (var key : sysProps.stringPropertyNames()) { configuration.put(key, decryptIfNeeded(sysProps.getProperty(key))); } } /** * Processes command line arguments and adds them to the configuration. * Arguments should be in the format "--key=value". * Any encrypted values are decrypted before being added. * * @param args An array of command line arguments. */ private void loadCommandLineArguments(String[] args) { for (var arg : args) { if (arg.startsWith("--")) { String[] keyValue = arg.substring(2).split("=", 2); if (keyValue.length == 2) { configuration.put(keyValue[0], decryptIfNeeded(keyValue[1])); } } } } /** * Resolves placeholders in all configuration values. * Placeholders are in the format ${key} and are replaced with their corresponding values. */ private void resolvePlaceholders() { for (var entry : configuration.entrySet()) { entry.setValue(resolvePlaceholder(entry.getValue())); } } /** * Resolves placeholders in a single string value. * Placeholders are in the format ${key} and are replaced with their corresponding values. * * @param value The string value potentially containing placeholders. * @return The input string with all placeholders resolved, or null if the input is null. */ private String resolvePlaceholder(String value) { if (value == null) return null; var matcher = patternVar.matcher(value); var sb = new StringBuilder(); while (matcher.find()) { var key = matcher.group(1); var replacement = getProperty(key); matcher.appendReplacement(sb, replacement != null ? replacement : matcher.group()); } matcher.appendTail(sb); return sb.toString(); } /** * Gets the current configuration name. * * @return The name of the current configuration. */ public String getConfigName() { return configName; } /** * Sets the configuration name. * * @param configName The new name for the configuration. */ public void setConfigName(String configName) { this.configName = configName; } /** * Sets the property encryptor to be used for encrypting and decrypting sensitive values. * * @param propertyEncryptor The PropertyEncryptor implementation to be used. */ public void setPropertyEncryptor(PropertyEncryptor propertyEncryptor) { this.propertyEncryptor = propertyEncryptor; } /** * Sets whether exceptions should be thrown when configuration loading fails. * * @param throwException If true, exceptions will be thrown on configuration loading failures. * If false, failures will be logged but not thrown. */ public void setThrowException(boolean throwException) { this.throwException = throwException; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy