Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEXI-2491: implemented configuration utility #2

Merged
merged 19 commits into from
Nov 16, 2018
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
mortengf marked this conversation as resolved.
Show resolved Hide resolved
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>

<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
Expand All @@ -33,6 +40,22 @@
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
hofmeister marked this conversation as resolved.
Show resolved Hide resolved
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.19</version>
</dependency>

<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId>
Expand Down
187 changes: 187 additions & 0 deletions src/main/java/io/dexi/config/Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package io.dexi.config;

import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.FileBasedConfiguration;
import org.apache.commons.configuration2.YAMLConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.ReloadingFileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.builder.fluent.PropertiesBuilderParameters;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.lang.StringUtils;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
* This class supports reading a "hierarchical" configuration file with sections like:
*
* <b>YAML example</b>
* <pre>
* {@code
* dexi:
* baseUrl: http://localhost:3000/api/
* apiKey: super-secret-key
mortengf marked this conversation as resolved.
Show resolved Hide resolved
* }
* </pre>
*
* <b>JSON example</b>
* <pre>
* {@code
* {
* "dexi": {
* "baseUrl": "http://localhost:3000/api/",
* "apiKey": "super-secret-key"
mortengf marked this conversation as resolved.
Show resolved Hide resolved
* }
* }
* }
* </pre>
*
* The file can be read from a local disk or from a URL. The class also supports reading environment variables whose
* names start with "DEXI_APP_".
*
* Configuration is read in the following order:
* <ol>
* <li>A local configuration file as specified by the {@code localConfigFile} parameter. The default is
* {@code ~/.dexi/my-app.yml}.
* <ul>
* <li>Supported file formats are YAML (.yml), JSON (.json), XML (.xml) and INI (.ini).</li>
* </ul>
* </li>
* <li>If an environment variable or system property named {@code DEXI_APP_CONFIG_URL_YML} is set, read a YAML (.yml)
* file from the specified URL.</li>
* <li>Read any {@code DEXI_APP_} environment variable. The format is: {@code DEXI_APP_&lt;section>_&lt;key> = &lt;value>}.</li>
* </ol>
*
* Values for duplicate keys within sections are overwritten by later keys.
*
*/
public class Config {

public static final String DEXI_APP_CONFIG_URL = "DEXI_APP_CONFIG_URL";
mortengf marked this conversation as resolved.
Show resolved Hide resolved

private static final String DEXI_APP_ENVIRONMENT_VARIABLE_PREFIX = "DEXI_APP_";

private static String localConfigFile = System.getProperty("user.home") + "/.dexi/my-app.yml";
mortengf marked this conversation as resolved.
Show resolved Hide resolved
private static Properties properties = new Properties();

private static void readEnvironment() {
Map<String, String> env = System.getenv();
Set<String> envKeys = env.keySet();
if (envKeys.size() > 0) {
for (String envKey : envKeys) {
if (envKey.startsWith(DEXI_APP_ENVIRONMENT_VARIABLE_PREFIX) && !envKey.startsWith(DEXI_APP_CONFIG_URL)) {
String envKeyWithoutPrefix = envKey.substring(5).toLowerCase();
if (envKeyWithoutPrefix.indexOf("_") == -1) {
continue;
}

String section = envKeyWithoutPrefix.substring(0, envKeyWithoutPrefix.indexOf("_"));
String key = envKeyWithoutPrefix.substring(envKeyWithoutPrefix.indexOf("_") + 1);
String value = env.get(envKey);

String keyWithSection = String.format("%s.%s", section, key);
properties.setProperty(keyWithSection, value);
}
}
}
}

private static <T extends FileBasedConfiguration> Configuration getConfigurationFile(String fileLocation, Class<T> filedBasedClazz) throws ConfigurationException, URISyntaxException, MalformedURLException {
T configuration = null;

Parameters parameters = new Parameters();
PropertiesBuilderParameters properties = parameters.properties();
FileBasedConfigurationBuilder<T> builder = new ReloadingFileBasedConfigurationBuilder<>(filedBasedClazz);

URI uri = new URI(fileLocation);
String scheme = uri.getScheme();
if ("http".equalsIgnoreCase(scheme)) {
builder = builder.configure(properties.setURL(uri.toURL()));
configuration = builder.getConfiguration();
} else {
URL localFileURL = Config.class.getResource(fileLocation);
if (localFileURL != null) {
builder = builder.configure(properties.setFileName(localFileURL.getFile()));
configuration = builder.getConfiguration();
}
}

return configuration;
}

private static void addConfigurationToProperties(Configuration configuration) {
if (configuration != null) {
Iterator<String> keys = configuration.getKeys();
while (keys.hasNext()) {
String keyWithSection = keys.next();

String existingValue = properties.getProperty(keyWithSection);
if (existingValue == null) {
String value = configuration.getString(keyWithSection);
properties.put(keyWithSection, value);
}
}
}
}

private static void getConfigurationFromURL() throws MalformedURLException, ConfigurationException, URISyntaxException {
String dexiAppConfigUrl = System.getenv(DEXI_APP_CONFIG_URL);
if (StringUtils.isEmpty(dexiAppConfigUrl)) {
dexiAppConfigUrl = System.getProperty(DEXI_APP_CONFIG_URL);
}

if (StringUtils.isNotEmpty(dexiAppConfigUrl)) {
Configuration ymlConfigurationURL = getConfigurationFile(dexiAppConfigUrl, YAMLConfiguration.class);
addConfigurationToProperties(ymlConfigurationURL);
}
}

private static void readLocalConfiguration() throws MalformedURLException, ConfigurationException, URISyntaxException {
String fileExtension = localConfigFile.substring(localConfigFile.lastIndexOf(".") + 1);

Class<? extends FileBasedConfiguration> configurationClass;
switch (fileExtension) {
case "yml":
configurationClass = YAMLConfiguration.class;
break;
case "json":
configurationClass = YAMLConfiguration.class;
break;
case "xml":
configurationClass = YAMLConfiguration.class;
break;
case "ini":
configurationClass = YAMLConfiguration.class;
break;
default:
throw new IllegalArgumentException("Unsupported file extension " + fileExtension);
}

Configuration ymlConfigurationLocal = getConfigurationFile(localConfigFile, configurationClass);
addConfigurationToProperties(ymlConfigurationLocal);
}

public static void load() throws ConfigurationException, URISyntaxException, MalformedURLException {
readLocalConfiguration();
mortengf marked this conversation as resolved.
Show resolved Hide resolved

getConfigurationFromURL();

readEnvironment();
}

public static Properties getProperties() {
return properties;
}

public static void setLocalConfigFile(String localConfigFile) {
Config.localConfigFile = localConfigFile;
}

}
52 changes: 52 additions & 0 deletions src/test/java/io/dexi/config/ConfigTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.dexi.config;

import org.apache.commons.configuration2.ex.ConfigurationException;
import org.junit.After;
import org.junit.Test;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.Properties;

import static org.junit.Assert.assertEquals;

public class ConfigTest {

@After
public void clearProperties() {
Config.getProperties().clear();
}

@Test
public void testReadingLocalFiles() throws ConfigurationException, URISyntaxException, MalformedURLException {
Config.setLocalConfigFile("/test-app.yml");

Config.load();

Properties properties = Config.getProperties();
assertEquals(3, properties.keySet().size());
}

@Test
public void testReadingFileFromURL() throws ConfigurationException, URISyntaxException, MalformedURLException {
Config.setLocalConfigFile("/non-existing-local-config-file.json");
System.setProperty(Config.DEXI_APP_CONFIG_URL, "http://config.dexi.io:1080/dexi-config/test/ini/apps/app-service-s3.yml");

Config.load();

Properties properties = Config.getProperties();
assertEquals(3, properties.keySet().size());
}

@Test
public void testReadingLocalFilesAndFileFromURL() throws ConfigurationException, MalformedURLException, URISyntaxException {
Config.setLocalConfigFile("/test-app.json");
System.setProperty(Config.DEXI_APP_CONFIG_URL, "http://config.dexi.io:1080/dexi-config/test/ini/apps/app-service-s3.yml");

Config.load();

Properties properties = Config.getProperties();
assertEquals(6, properties.keySet().size());
}

}
9 changes: 9 additions & 0 deletions src/test/resources/test-app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"dexi": {
"baseUrl": "http://localhost:3000/api/",
"apiKey": "super-secret-key"
},
"google": {
"baseUrl": "https://www.googleapis.com/storage/v1/"
}
}
6 changes: 6 additions & 0 deletions src/test/resources/test-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dexi:
baseUrl: http://localhost:3000/api/
apiKey: super-secret-key

google:
baseUrl: https://www.googleapis.com/storage/v1/