package com.kryptnostic.rhizome.configuration.service;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amazonaws.services.s3.AmazonS3;
import com.auth0.jwt.internal.org.apache.commons.io.IOUtils;
import com.dataloom.mappers.ObjectMappers;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.google.common.io.Resources;
import com.kryptnostic.rhizome.configuration.ConfigurationKey;
import com.kryptnostic.rhizome.configuration.SimpleConfigurationKey;
import com.kryptnostic.rhizome.configuration.annotation.ReloadableConfiguration;
/**
* Configuration service API for getting, setting, and registering for configuration updates.
*
* @author Matthew Tamayo-Rios
*/
public interface ConfigurationService {
public void registerModule( Module module );
/**
* Retrieves an existing configuration. The configuration class must have a static {@code getKey()} method that will
* be used as key to lookup the YAML configuration.
*
* @param clazz
* @return The configuration if it can be found, null otherwise.
* @throws IOException
*/
public abstract @Nullable <T> T getConfiguration( Class<T> clazz ) throws IOException;
/**
* Creates or updates a configuration and fires a configuration update event to all subscribers.
*
* @param configuration The configuration to be updated or created
* @throws IOException
*/
public abstract <T> void setConfiguration( T configuration ) throws IOException;
/**
* Registers a subscriber that will receive updated configuration events at methods annotated with guava's
* {@code @Subscribe} annotation.
*
* @param subscriber
*/
// public abstract <T extends Configuration> s<Configuration> getAllConfigurations();
public abstract void subscribe( Object subscriber );
public final static class StaticLoader {
private static final Logger logger = LoggerFactory.getLogger( StaticLoader.class );
private static final ObjectMapper mapper = ObjectMappers.getYamlMapper();
private StaticLoader() {}
/**
* Static method for loading a configuration before the service has been instantiated. This is useful for
* bootstrapping an application context or a database connection, which the Configuration service may depend on.
* The configuration class must have a static {@code getKey()} method that will be used as the name of the
* resource containing the YAML configuration.
*
* @param clazz - The configuration class to load.
* @return The configuration if it can successfully loaded, null otherwise.
*/
public static @Nullable <T> T loadConfiguration( Class<T> clazz ) {
ConfigurationKey key = getConfigurationKey( Preconditions.checkNotNull(
clazz,
"Cannot load configuration for null class." ) );
if ( key == null ) {
logger.error( "Unable to load key for configuration class {}", clazz.getName() );
return null;
}
return loadConfigurationFromResource( key, clazz );
}
public static @Nullable ConfigurationKey getConfigurationKey( Class<?> clazz ) {
String uri = getReloadableConfigurationUri( clazz );
if ( StringUtils.isNotBlank( uri ) ) {
return new SimpleConfigurationKey( uri );
}
/*
* This requires a static method called key on the class. Unfortunately, in Java 7 it cannot be enforced.
*/
try {
Method keyGetter = Preconditions.checkNotNull( clazz.getMethod( "key" ), clazz.getName()
+ " is missing required static method key()." );
return (ConfigurationKey) keyGetter.invoke( null );
} catch ( InvocationTargetException nfe ) {
logger.error( clazz.getName() + " is missing required static method key().", nfe );
return null;
} catch ( Exception e ) {
logger.error( "Unable to determine configuration id for class " + clazz.getName(), e );
return null;
}
}
static String getReloadableConfigurationUri( Class<?> clazz ) {
ReloadableConfiguration config = clazz.getAnnotation( ReloadableConfiguration.class );
if ( config != null ) {
if ( StringUtils.isBlank( config.uri() ) ) {
return clazz.getCanonicalName();
} else {
return config.uri();
}
} else {
return null;
}
}
static @Nullable <T> T loadConfigurationFromResource(
ConfigurationKey key,
Class<T> clazz ) {
T s = null;
String yamlString = null;
try {
try {
URL resource = Resources.getResource( key.getUri() );
yamlString = Resources.toString( resource, StandardCharsets.UTF_8 );
if ( StringUtils.isBlank( yamlString ) ) {
throw new IOException( "Unable to read configuration from classpath." );
}
} catch ( IOException | IllegalArgumentException e ) {
logger.warn( "Failed to load resource from " + key.getUri(), e );
}
s = mapper.readValue( yamlString, clazz );
} catch ( IOException e ) {
logger.error( "Failed to load default configuration for " + key.getUri(), e );
}
return s;
}
public static @Nullable <T> T loadConfigurationFromS3(
AmazonS3 s3,
String bucket,
String folder,
Class<T> clazz ) {
T s = null;
String key = getReloadableConfigurationUri( clazz );
String yamlString = null;
try {
try {
yamlString = IOUtils.toString( s3.getObject( bucket, folder + key ).getObjectContent() );
} catch ( IOException | IllegalArgumentException e ) {
logger.debug( "Failed to load resource from " + key, e );
}
if ( StringUtils.isBlank( yamlString ) ) {
throw new IOException( "Unable to read configuration from classpath." );
}
s = mapper.readValue( yamlString, clazz );
} catch ( IOException e ) {
logger.debug( "Failed to load default configuration for " + key, e );
}
return s;
}
}
}