com.github.cafapi.common.config.source.CafConfigurationSource Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2015-2024 Open Text.
*
* 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 com.github.cafapi.common.config.source;
import com.github.cafapi.common.api.BootstrapConfiguration;
import com.github.cafapi.common.api.Cipher;
import com.github.cafapi.common.api.CipherException;
import com.github.cafapi.common.api.CodecException;
import com.github.cafapi.common.api.Configuration;
import com.github.cafapi.common.api.ConfigurationException;
import com.github.cafapi.common.api.Decoder;
import com.github.cafapi.common.api.Encrypted;
import com.github.cafapi.common.api.ManagedConfigurationSource;
import com.github.cafapi.common.util.naming.Name;
import com.github.cafapi.common.util.naming.ServicePath;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.text.lookup.StringLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
/**
* Partial implementation of a ManagedConfigurationSource that performs hierarchical lookups based upon the service's ServicePath, and
* recursive lookup for configuration objects that themselves have configuration in marked with the @Configuration annotation.
*/
public abstract class CafConfigurationSource implements ManagedConfigurationSource
{
private final Cipher security;
private final ServicePath id;
private final Decoder decoder;
private final boolean isSubstitutorEnabled;
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
private final AtomicInteger confRequests = new AtomicInteger(0);
private final AtomicInteger confErrors = new AtomicInteger(0);
private static final Logger LOG = LoggerFactory.getLogger(CafConfigurationSource.class);
/**
* Each ConfigurationProvider itself takes some initial source of configuration which it may or may not use to initialise itself. The
* initial "bootstrap" configuration comes from the worker core itself.
*
* @param bootstrapProvider the initial provider of configuration
* @param cipher for decrypting information in a configuration file
* @param servicePath to localise configuration for this service
* @param decoder provides a mechanism to decode the configuration format
*/
public CafConfigurationSource(final BootstrapConfiguration bootstrapProvider, final Cipher cipher, final ServicePath servicePath,
final Decoder decoder)
{
this.security = Objects.requireNonNull(cipher);
this.id = Objects.requireNonNull(servicePath);
this.decoder = Objects.requireNonNull(decoder);
Objects.requireNonNull(bootstrapProvider);
this.isSubstitutorEnabled = getIsSubstitutorEnabled(bootstrapProvider);
}
/**
* Acquire a configuration class from the provider. The requested class will be a simple Java object that when returned, and can be
* interacted with using getters and other standard mechanisms. Configuration classes may themselves contain other configuration
* objects, which will be recursively acquired if marked @Configuration. Any fields marked @Encrypted will be decrypted, any fields
* marked and any validation annotations will be processed.
*
* @param configClass the class that represents your configuration
* @param the class that represents your configuration
* @return the configuration class requested, if it can be deserialised
* @throws ConfigurationException if the configuration class cannot be acquired or deserialised
*/
@Override
public final T getConfiguration(final Class configClass)
throws ConfigurationException
{
Objects.requireNonNull(configClass);
incrementRequests();
T config = getCompleteConfig(configClass);
Set> violations = getValidator().validate(config);
if (violations.isEmpty()) {
return config;
} else {
incrementErrors();
LOG.error("Configuration constraint violations found for {}: {}", configClass.getSimpleName(), violations);
throw new ConfigurationException("Configuration validation failed for " + configClass.getSimpleName());
}
}
/**
* {@inheritDoc}
*/
@Override
public final int getConfigurationRequests()
{
return confRequests.get();
}
/**
* {@inheritDoc}
*/
@Override
public final int getConfigurationErrors()
{
return confErrors.get();
}
protected Cipher getCipher()
{
return this.security;
}
protected ServicePath getServicePath()
{
return this.id;
}
protected Validator getValidator()
{
return this.validator;
}
/**
* Acquire and return a stream of the serialised data from the transport source.
*
* @param configClass the configuration class to be acquired
* @param relativePath the partial service path that defines the scope to try and acquire the configuration in
* @return the stream containing the serailised configuration of the class
* @throws ConfigurationException if the stream cannot be acquired
*/
protected abstract InputStream getConfigurationStream(final Class configClass, final Name relativePath)
throws ConfigurationException;
/**
* This is the recursive entry point for acquiring a complete configuration class to return. Attempt to acquire a deserialised object
* representing the configuration class requested, and analyse it for declared fields marked @Configuration. If any are found, the
* method recursively calls itself until all configuration is satisfied.
*
* @param configClass the class representing configuration to acquire
* @param the class representing configuration to acquire
* @return the completed (at this level) configuration
* @throws ConfigurationException if configuration cannot be acquired
*/
private T getCompleteConfig(final Class configClass)
throws ConfigurationException
{
T config = getConfig(configClass);
for (final Field f : configClass.getDeclaredFields()) {
if (f.isAnnotationPresent(Configuration.class)) {
try {
Method setter = getMethod(f.getName(), configClass, PropertyDescriptor::getWriteMethod);
if (setter != null) {
setter.invoke(config, getCompleteConfig(f.getType()));
}
} catch (final ConfigurationException e) {
LOG.debug("Didn't find any overriding configuration", e);
} catch (final InvocationTargetException | IllegalAccessException e) {
incrementErrors();
throw new ConfigurationException("Failed to get complete configuration for " + configClass.getSimpleName(), e);
}
} else if (f.getType().equals(String.class) && f.isAnnotationPresent(Encrypted.class)) {
try {
Method getter = getMethod(f.getName(), config.getClass(), PropertyDescriptor::getReadMethod);
Method setter = getMethod(f.getName(), config.getClass(), PropertyDescriptor::getWriteMethod);
if (getter != null && setter != null) {
final String configValue = (String) getter.invoke(config);
final String encryptedValue = isSubstitutorEnabled ? tokenSubstitutor(configValue) : configValue;
setter.invoke(config, getCipher().decrypt(encryptedValue));
}
} catch (final CipherException | InvocationTargetException | IllegalAccessException e) {
throw new ConfigurationException("Failed to decrypt class fields", e);
}
} else if (isSubstitutorEnabled && f.getType().equals(String.class)) {
try {
String propertyName = f.getName();
Method getter = getMethod(propertyName, config.getClass(), PropertyDescriptor::getReadMethod);
Method setter = getMethod(propertyName, config.getClass(), PropertyDescriptor::getWriteMethod);
if (getter != null && setter != null) {
// Property value may contain tokens that require substitution.
String propertyValueByToken = tokenSubstitutor((String) getter.invoke(config));
setter.invoke(config, propertyValueByToken);
}
} catch (final InvocationTargetException | IllegalAccessException e) {
throw new ConfigurationException("Failed to get complete configuration for " + configClass.getSimpleName(), e);
}
}
}
return config;
}
/**
* Acquire, decode and decrypt a configuration object from a data stream.
*
* @param configClass the class representing configuration to acquire
* @param the class representing configuration to acquire
* @return the decoded configuration object
* @throws ConfigurationException if the configuration cannot be acquired
*/
private T getConfig(final Class configClass)
throws ConfigurationException
{
Iterator it = getServicePath().descendingPathIterator();
while (it.hasNext()) {
try (InputStream in = getConfigurationStream(configClass, it.next())) {
return decoder.deserialise(in, configClass);
} catch (final ConfigurationException e) {
LOG.trace("No configuration at this path level", e);
} catch (final CodecException | IOException e) {
incrementErrors();
throw new ConfigurationException("Failed to get configuration for " + configClass.getSimpleName(), e);
}
}
incrementErrors();
throw new ConfigurationException("No configuration found for " + configClass.getSimpleName());
}
/**
* Checks whether the string substitution functionality should be enabled.
*
* @param bootstrapConfig used to provide basic, initial startup configuration
* @return true if the configuration substitution should be performed
*/
private static boolean getIsSubstitutorEnabled(final BootstrapConfiguration bootstrapConfig)
{
final String ENABLE_SUBSTITUTOR_CONFIG_KEY = "CAF_CONFIG_ENABLE_SUBSTITUTOR";
final boolean ENABLE_SUBSTITUTOR_CONFIG_DEFAULT = true;
// Return the default if the setting is not configured
if (!bootstrapConfig.isConfigurationPresent(ENABLE_SUBSTITUTOR_CONFIG_KEY)) {
return ENABLE_SUBSTITUTOR_CONFIG_DEFAULT;
}
// Return the configured setting.
// The ConfigurationException should never happen since isConfigurationPresent() has already been called.
try {
return bootstrapConfig.getConfigurationBoolean(ENABLE_SUBSTITUTOR_CONFIG_KEY);
} catch (final ConfigurationException ex) {
throw new RuntimeException(ex);
}
}
private static String tokenSubstitutor(final String source)
{
final StringSubstitutor strSubstitutor = new StringSubstitutor(
new StringLookup()
{
@Override
public String lookup(final String key)
{
return (System.getProperty(key) != null) ? System.getProperty(key) : System.getenv(key);
}
});
return strSubstitutor.replace(source);
}
/**
* Increase the number of configuration requests recorded.
*/
protected void incrementRequests()
{
this.confRequests.incrementAndGet();
}
/**
* Increase the number of configuration errors recorded.
*/
protected void incrementErrors()
{
this.confErrors.incrementAndGet();
}
private Method getMethod(
final String propertyName,
final Class> beanClass,
final Function function
)
{
try {
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(propertyName, beanClass);
return function.apply(propertyDescriptor);
} catch (final IntrospectionException e) {
LOG.debug(String.format("Unable to "
+ "create Property Descriptor from field %s :", propertyName) + System.lineSeparator()
+ ExceptionUtils.getStackTrace(e));
return null;
}
}
}