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

com.accenture.wconf.WaterfallConfig Maven / Gradle / Ivy

The newest version!
package com.accenture.wconf;

import static com.accenture.wconf.MetaConfigKeys.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueType;

/**
 * Eagerly loaded Singleton supporting wconf()
 * 
 * Use wconf() to access the API that provide access to the configuration properties
 *
 */
public class WaterfallConfig {
	
	private static Logger LOGGER = LoggerFactory.getLogger(WaterfallConfig.class);
	
	private static WaterfallConfig uniqueInstance = new WaterfallConfig();
	
	private UUID instanceUUID;
	
	private boolean isEncryptionEnabled;
	private Config config;
	private Cipher cipher;
		
	private String REFERENCE_RESOURCE = "config/common.conf";
	private String DEFAULT_APPLICATION_RESOURCE = "config/application.conf";

	
	private WaterfallConfig() {
		Instant start = Instant.now();
		instanceUUID = UUID.randomUUID();
		LOGGER.debug("Initializing a new Waterfall config: {}", instanceUUID = UUID.randomUUID());

		/* Load common props as a resource: It's assumed it's found inside the JAR */
		Config commonProps = ConfigFactory.parseResourcesAnySyntax(REFERENCE_RESOURCE).resolve();
		
		/* Load bootstrapConfig that will let us tailor how other config props are discovered */
		Config bootstrapConfig = ConfigFactory.systemEnvironment()
									.withFallback(ConfigFactory.systemProperties())
									.withFallback(commonProps);

		/* obtain application-level property (if any) and external application-level property (if any) */
		String appResource = bootstrapConfig.hasPath(META_CONFIG_APP_RESOURCE_KEY.toString())? bootstrapConfig.getString(META_CONFIG_APP_RESOURCE_KEY.toString()) : DEFAULT_APPLICATION_RESOURCE;

		
		/* Load props found outside the JAR: allowing scanning of additional external paths */
		Config applicationPropsFoundOutsideJar = getApplicationPropsFromExternalFile(bootstrapConfig, appResource);
		
		/* Load props from environment vars */
		Config environmentVariablesProps = ConfigFactory.systemEnvironment();
		
		/* Load props from Java system vars */
		Config javaSystemProps = ConfigFactory.systemProperties();
		
		/* Load props from app resource inside the Jar */
		Config applicationProps = ConfigFactory.parseResources(appResource).resolve();
		
		/* Establish precedence rules */
		Config conf = applicationPropsFoundOutsideJar
						.withFallback(environmentVariablesProps)
						.withFallback(javaSystemProps)
						.withFallback(applicationProps)
						.withFallback(commonProps);
		
		/* restrict conf properties to the specified profile if found */	
		if (conf.hasPath(META_CONFIG_ACTIVE_PROFILE_KEY.toString())) {
			config = applyProfileToConfig(conf, applicationPropsFoundOutsideJar, environmentVariablesProps, javaSystemProps, applicationProps, commonProps);
		} else {
			config = conf;
		}		
		
		/* Apply encryption if enabled */
		isEncryptionEnabled = config.hasPath(META_CONFIG_ENCRYPTION_SWITCH_KEY.toString()) ? config.getBoolean(META_CONFIG_ENCRYPTION_SWITCH_KEY.toString()) : false;		
		if (isEncryptionEnabled) {
			loadEncryptionConfiguration();
		}
		

				
		Duration duration = Duration.between(start, Instant.now());
		LOGGER.debug("Config initialization took {}", duration);
		LOGGER.debug("Encryption configured: {}", isEncryptionEnabled);
		
	}
	
	private Config applyProfileToConfig(Config conf, Config applicationPropsFoundOutsideJar, Config environmentVariablesProps, Config javaSystemProps, Config applicationProps, Config commonProps) {
		Config configRestrictedToActiveProfile;
		String activeProfile = conf.getString(META_CONFIG_ACTIVE_PROFILE_KEY.toString());						
		if (applicationPropsFoundOutsideJar.hasPath(activeProfile)) {
			configRestrictedToActiveProfile = applicationPropsFoundOutsideJar.getConfig(activeProfile)
						.withFallback(environmentVariablesProps)
						.withFallback(javaSystemProps);
		} else {
			configRestrictedToActiveProfile = environmentVariablesProps
						.withFallback(javaSystemProps);
		}			
		if (applicationProps.hasPath(activeProfile)) {
			configRestrictedToActiveProfile = configRestrictedToActiveProfile
						.withFallback(applicationProps.getConfig(activeProfile));
		}
		configRestrictedToActiveProfile = configRestrictedToActiveProfile
					.withFallback(commonProps);
		
		return configRestrictedToActiveProfile;
	}

	private void loadEncryptionConfiguration() {
		LOGGER.debug("Applying encryption configuration");
		String encryptionAlgorithm = config.getString(META_CONFIG_ENCRYPTION_ALGORITHM_KEY.toString());
		String keyType = config.getString(META_CONFIG_ENCRYPTION_KEY_TYPE_KEY.toString());
		String keystorePath = config.getString(META_CONFIG_ENCRYPTION_KEY_STORE_PATH_KEY.toString());
		String keyStorePassword = config.getString(META_CONFIG_ENCRYPTION_KEY_STORE_PASSWORD_KEY.toString());
		String configSecretKeyAlias = config.getString(META_CONFIG_ENCRYPTION_KEY_STORE_KEY_ALIAS_KEY.toString());
		String configSecretKeyPassword = config.getString(META_CONFIG_ENCRYPTION_KEY_STORE_KEY_PASSWORD_KEY.toString());
		String encodedInitializationVector = config.getString(META_CONFIG_ENCRYPTION_IV_KEY.toString());
	
		try (InputStream keystoreStream = classpathAwareInputStreamFactory(keystorePath)) {
			KeyStore keyStore = KeyStore.getInstance("JCEKS");
			keyStore.load(keystoreStream, keyStorePassword.toCharArray());
			
			if (!keyStore.containsAlias(configSecretKeyAlias)) {
				LOGGER.error("The key {} was not found in the key store {}", configSecretKeyAlias, keyStore);
				throw new IllegalStateException("Could not find the expected key in the provided keystore");
			}
			
			Key aesKey = keyStore.getKey(configSecretKeyAlias, configSecretKeyPassword.toCharArray());	
			
			SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey.getEncoded(), keyType);
			cipher = Cipher.getInstance(encryptionAlgorithm);
			IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(encodedInitializationVector));
			
			cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
			LOGGER.debug("Encryption correctly initialized");
			
		} catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
			LOGGER.error("Could not initialize the encryption scheme from the provided keystore and config data", e);
			throw new IllegalStateException("Could not initialize the encryption scheme", e);
		}
	}

	private Config getApplicationPropsFromExternalFile(Config bootstrapConfig, String appResource) {
		String externalAppResource = Paths.get(appResource).getFileName().toString();
		LOGGER.debug("Checking if external file config prop file is present: {}", externalAppResource);

		List externalPaths = new ArrayList();
		if (bootstrapConfig.hasPath(META_CONFIG_EXT_APP_RESOURCE_ADDITIONAL_PATHS.toString())) {
			if (bootstrapConfig.getValue(META_CONFIG_EXT_APP_RESOURCE_ADDITIONAL_PATHS.toString()).valueType() == ConfigValueType.STRING) {
				externalPaths.add(bootstrapConfig.getString(META_CONFIG_EXT_APP_RESOURCE_ADDITIONAL_PATHS.toString()));				
			} else {
				externalPaths.addAll(bootstrapConfig.getStringList(META_CONFIG_EXT_APP_RESOURCE_ADDITIONAL_PATHS.toString()));				
			}
		} else {
			externalPaths.add("./");
		}
		LOGGER.info("Checking the following paths for external config file: {}", externalPaths);
		Config applicationPropsFoundOutsideJar = null;
		for (String externalAppPathPrefix : externalPaths) {
			Path externalPropFilePath = Paths.get(externalAppPathPrefix, externalAppResource);
			applicationPropsFoundOutsideJar = ConfigFactory.parseFile(externalPropFilePath.toFile());
			if (!applicationPropsFoundOutsideJar.isEmpty()) {
				LOGGER.info("External application properties file found in {}", externalPropFilePath.toAbsolutePath());
				break;
			}
		}
		
		if (applicationPropsFoundOutsideJar.isEmpty()) {
			LOGGER.info("No configuration properties found outside the JAR for {} in {}", externalAppResource, externalPaths);
		}
		return applicationPropsFoundOutsideJar;
	}

	/**
	 * Access method to the configuration object
	 *  
	 * @return an instance of the configuration object that provide access to the configuration properties
	 */
	public static WaterfallConfig wconf() {
		return uniqueInstance;
	}
	
	/**
	 * Obtains the string representation of a configuration parameter.
	 * 
	 * @param key the key of the configuration parameter to retrieve
	 * @return the string value associated to the key
	 */
	public String get(String key) {
		Instant start = Instant.now();
		String value = getConfig(key);
		LOGGER.debug("Access to config {} to get {} took {}", uniqueInstance.instanceUUID, key, Duration.between(start, Instant.now()));		
		return value;
	}
	
	/**
	 * Obtains the string representation of a configuration parameter or Optional.empty()
	 * 
	 * @param key the key of the configuration parameter to retrieve
	 * @return optional of string value associated to key
	 */
	public Optional safeGet(String key) {
		Instant start = Instant.now();
		Optional result;
		try {
			result = Optional.of(get(key));
		} catch (ConfigException e) {
			LOGGER.warn("No value was found for key {}: an Optional.empty() will be returned", key);
			result = Optional.empty();
		}
		LOGGER.debug("Access to config {} to safeGet {} took {}", uniqueInstance.instanceUUID, key, Duration.between(start, Instant.now()));
		return result;
	}
	
	/**
	 * Obtains the string representation of a configuration parameter or the default value if it does not exist
	 * 
	 * @param key the key of the configuration parameter to retrieve
	 * @param defaultValue the value to return if no configuration for the key is present
	 * @return the string value associated to key
	 */
	public String getOrElse(String key, String defaultValue) {
		Instant start = Instant.now();
		String result;
		try {
			result = get(key);
		} catch (ConfigException e) {
			LOGGER.warn("No value was found for key {}: the default value {} will be returned", key, defaultValue);
			result = defaultValue;
		}
		LOGGER.debug("Access to config {} to getOrElse {} took {}", uniqueInstance.instanceUUID, key, Duration.between(start, Instant.now()));		
		return result;		
	}
	

	/**
	 * Obtains the list of strings for a given configuration property key 
	 * @param key the property key of the value to retrieved
	 * @param isMultivalued a dummy parameter used to indicate we're interested in a multivalued property
	 * @return the list of values associated to the given property key
	 */
	public List get(String key, boolean isMultivalued) {
		Instant start = Instant.now();
		List values = uniqueInstance.config.getStringList(key);
		LOGGER.debug("Access to config multivalued {} to get {} took {}", uniqueInstance.instanceUUID, key, Duration.between(start, Instant.now()));
		return values;
	}
	
	/**
	 * Obtains the list of strings for a given configuration property key or an Optional.empty() if no value was found
	 * @param key the property key of the value to retrieved
	 * @param isMultiValued a dummy parameter used to indicate we're interested in a multivalued property
	 * @return the list of values associated to the given property key
	 */
	public Optional> safeGet(String key, boolean isMultiValued) {
		if (key == null || key.isEmpty()) {
			LOGGER.error("safeGet multivalued requires a non-null/non-empty argument");
			throw new IllegalArgumentException("safeGet multivalued requires a non-null/non-empty argument");
		}
		Instant start = Instant.now();
		Optional> result;
		try {
			result = Optional.of(uniqueInstance.config.getStringList(key));
		} catch (ConfigException e) {
			LOGGER.warn("No multi-value was found for key {}: an Optional.empty() will be returned", key);
			result = Optional.empty();
		}
		LOGGER.debug("Access to config multivalued {} to safeGet {} took {}", uniqueInstance.instanceUUID, key, Duration.between(start, Instant.now()));		
		return result;
	}
	
	/**
	 * Obtains the list of strings for a given configuration property key or the default value passed if no value was found
	 * @param key the property key of the value to retrieved
	 * @param defaultValue the value to return if no configuration for the key is present 
	 * @param isMultiValued a dummy parameter used to indicate we're interested in a multivalued property
	 * @return the list of values associated to the given property key
	 */	
	public List getOrElse(String key, List defaultValue, boolean isMultiValued) {
		if (key == null || key.isEmpty()) {
			LOGGER.error("getOrElse multivalued requires a non-null/non-empty argument");
			throw new IllegalArgumentException("getOrElse multivalued requires a non-null/non-empty argument");			
		}
		List result;
		try {
			result = get(key, isMultiValued);
		} catch (ConfigException e) {
			LOGGER.warn("No multi-value was found for key {}: an Optional.empty() will be returned", key);
			result = defaultValue;
		}
		return result;
	}
	
	private static InputStream classpathAwareInputStreamFactory(String filePath) throws IOException {
		if (filePath.startsWith("classpath://")) {
			String internalPath = filePath.substring("classpath://".length());
			return WaterfallConfig.class.getClassLoader().getResourceAsStream(internalPath);			
		} else {
			return new FileInputStream(filePath);
		}		
	}
	
	private String getConfig(String key) {
		if (key == null || key.isEmpty()) {
			LOGGER.error("The given configuration key is null or empty");
			throw new IllegalArgumentException("The given configuration key is null or empty");
		}
		
		String value = uniqueInstance.config.getString(key);
	
		if (value.startsWith("cipher(") && value.endsWith(")")) {
			if (!isEncryptionEnabled) {
				throw new IllegalStateException("Encryption has not been enabled.");
			}
			String cipherText = value.substring(7, value.length() - 1);
			byte[] clearBytes;
			try {
				clearBytes = cipher.doFinal(Base64.getDecoder().decode(cipherText));
			} catch (IllegalBlockSizeException | BadPaddingException e) {
				LOGGER.error("Error trying to decrypt key {}", key);
				throw new IllegalArgumentException("Could not decrypt config value", e);
			}
			value = new String(clearBytes, StandardCharsets.UTF_8);
		}
		return value;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy