Skip to content

Commit

Permalink
Initial commit; support for lightbend config and clean separation of …
Browse files Browse the repository at this point in the history
…API, SPI, and core implementation
  • Loading branch information
xp-cagey committed Jun 24, 2018
1 parent 01fe322 commit 1c320c8
Show file tree
Hide file tree
Showing 100 changed files with 4,672 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Maven target
/cagey-config-api/target/
/cagey-config-spi/target/
/cagey-config-lightbend/target/
/cagey-config-core/target/

# IDE
*.iml
*.ipr
*.iws

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Cagey Config

[![Build Status](https://travis-ci.org/xp-cagey/cagey-config.svg?branch=master)](https://travis-ci.org/xp-cagey/cagey-config) [![codecov](https://codecov.io/gh/xp-cagey/cagey-config/branch/master/graph/badge.svg)](https://codecov.io/gh/xp-cagey/cagey-config) [![Maven Central](https://img.shields.io/maven-central/v/com.xpcagey/cagey-config.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.xpcagey%22%20AND%20a%3A%22cagey-config%22)

This is an abstraction layer for configuration that allows programs to subscribe to value changes and be notified when they occur, permitting live tuning of application logic and automatic stitching of configuration values from multiple sources. It does for configuration what SLF4J has done for logging.

Each application must declare a set of preferences for configuration sources, with each source able to contribute to the parameterization of the next. Configuration sources are each injected by a runtime module that should be placed into the application classpath before running the system. Failure to load a module will not cause the process to fail, but an exception will be thrown to report failures; it is up to the application to decide whether this
failure should be considered fatal.

An implementation of the system for static declaration of default values is provided in the core package.
13 changes: 13 additions & 0 deletions cagey-config-api/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>cagey-config-api</artifactId>

<parent>
<groupId>com.xpcagey</groupId>
<artifactId>cagey-config</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.xpcagey.config.api;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class CircularRequirementsException extends ConfigLoadException {
public final Set<Descriptor> desc;
public CircularRequirementsException(Collection<Descriptor> desc) {
super(buildMessage(desc));
this.desc = new HashSet<>(desc);
}

private static String buildMessage(Collection<Descriptor> desc) {
StringBuilder builder = new StringBuilder();
for(Descriptor d : desc) {
if (builder.length() == 0)
builder.append("Found circular references while attempting to load [");
else
builder.append("], [");
builder.append(d.toString());
}
builder.append("]");
return builder.toString();
}
}
96 changes: 96 additions & 0 deletions cagey-config-api/src/main/java/com/xpcagey/config/api/Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.xpcagey.config.api;

import java.util.Iterator;
import java.util.Optional;
import java.util.SortedMap;
import java.util.function.Consumer;

public interface Config extends Iterable<Element>, AutoCloseable {
/**
* Returns the name of this configuration; useful if a program wants to have distinct configuration blocks that each
* correspond to a different context.
* @return the name of this configuration that was specified at load time
*/
String getName();

/**
* Lists the items that were used to construct this configuration; useful for debugging
* @return the sources for value lookup used by this config in priority order (highest to lowest)
*/
Iterator<String> getSources();

/**
* Queries the configuration to see if it currently has an element at the specified key
* @param key the key to search
* @return true if the key exists
*/
default boolean hasKey(String key) { return getOrNull(key) != null; }

/**
* Returns a snapshot of the current elements inside this configuration; if there were multiple descriptors used to
* build a compound configuration the map will be flattened to show only the values that would be returned by
* <code>get</code> queries.
* @return the mapping of current values.
*/
SortedMap<String, Element> getAll();

/**
* Attempt to retrieve an element by key in a type safe manner that allows functional chaining; best used when a
* key is not required and the program needs to branch based on its presence
* @param key the key to search
* @return the element if found, or <code>Optional.empty()</code> if the element was not found.
*/
default Optional<Element> get(String key) { return Optional.ofNullable(getOrNull(key)); }

/**
* Attempt to retrieve an element by key efficiently by not wrapping for type safety if it is missing; best used
* in situations where temporary object creation overhead is not acceptable but the value is not required
* @param key the key to search
* @return the element if found, or <code>null</code> if the element was not found.
*/
Element getOrNull(String key);

/**
* Attempt to retrieve an element by key without needing conditional logic to interpret the result; best used when
* a default has been set for the key or when the program must have a value to operate correctly.
* @param key
* @return the element if found
* @throws MissingElementException if the element was not found
*/
default Element getOrThrow(String key) throws MissingElementException {
Element e = getOrNull(key);
if (e == null) throw new MissingElementException(getName(), key);
return e;
}

/**
* Places a change listener into the system. The listener will receive reports of any change in the current values
* supplied by configuration. This binding will *not* attempt to keep the callback from being garbage collected,
* and is safe to use with ephemeral objects.
* @param listener the code to be executed when a value updates.
*/
void addListener(Consumer<Element> listener);

/**
* Removes a change listener from the system.
* @param listener the code that was previously registered with <code>addListener</code>
*/
void removeListener(Consumer<Element> listener);

/**
* Places a listener for a single key into the system. If that key is already present, the config will immediately
* fire the trigger, allowing initialization and update to share a common code path. This binding will *not* attempt
* to keep the code from being garbage collected, and is safe to use with ephemeral objects. Triggers do not receive
* an update if the key is missing or removed from the <code>Config</code>
* @param key the key of the configuration parameter to track
* @param trigger the code to be executed on register and again when the value updates
*/
void addTrigger(String key, Consumer<Element> trigger);

/**
* Removes a listener for a single key from the system.
* @param key the key of the configuration parameter to stop tracking
* @param trigger the code that was previously registered with <code>addTrigger</code>
*/
void removeTrigger(String key, Consumer<Element> trigger);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.xpcagey.config.api;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Executor;

public interface ConfigEngine {
void setDefault(String key, boolean value);
void setDefault(String key, double value);
void setDefault(String key, Duration value);
void setDefault(String key, Instant value);
void setDefault(String key, long value);
void setDefault(String key, String value);

Config load(String name, Executor exec, Descriptor... descriptors) throws ConfigLoadException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.xpcagey.config.api;

/**
* ConfigLoadException is a common ancestor for loader exceptions that is provided for convenience
*/
public abstract class ConfigLoadException extends Exception {
ConfigLoadException(String message) { super(message); }
ConfigLoadException(String message, Throwable cause) { super(message, cause); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.xpcagey.config.api;

import java.time.Duration;
import java.time.Instant;
import java.util.Iterator;
import java.util.ServiceLoader;
import java.util.concurrent.Executor;

public final class ConfigSystem {
private ConfigSystem() {}

private static Iterable<ConfigEngine> loader = ServiceLoader.load(ConfigEngine.class);
private static final Object lock = new Object();
private static volatile ConfigEngine engine;

public static void setDefault(String key, boolean value) { bind().setDefault(key, value); }
public static void setDefault(String key, Duration value) { bind().setDefault(key, value); }
public static void setDefault(String key, Instant value) { bind().setDefault(key, value); }
public static void setDefault(String key, float value) { bind().setDefault(key, value); }
public static void setDefault(String key, int value) { bind().setDefault(key, value); }
public static void setDefault(String key, String value) { bind().setDefault(key, value); }

public static synchronized Config load(String name, Executor exec, Descriptor... descriptors) throws ConfigLoadException {
return bind().load(name, exec, descriptors);
}

private static ConfigEngine bind() {
ConfigEngine local = engine;
if (local == null) {
synchronized (lock) {
local = engine;
if (local == null)
engine = local = load();
}
}
return local;
}

private static ConfigEngine load() {
Iterator<ConfigEngine> engines = loader.iterator();
if (!engines.hasNext())
throw new IllegalStateException("No ConfigEngine has been found on the ClassPath");
ConfigEngine found = engines.next();
if (engines.hasNext())
throw new IllegalStateException("Multiple ConfigEngines have been found on the ClassPath");
return found;
}

// for unit test purposes only; this should not be used in production code.
static void reset(Iterable<ConfigEngine> testLoader) {
synchronized (lock) {
loader = testLoader != null ? testLoader : ServiceLoader.load(ConfigEngine.class);
engine = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.xpcagey.config.api;

import java.util.Collection;

public abstract class Descriptor implements Comparable<Descriptor> {
private final String provider;
private final String alias;
private final boolean required;
protected Descriptor(String provider, String alias, boolean required) {
this.provider = provider;
this.alias = alias;
this.required = required;
}

public String getProvider() { return this.provider; }
public String getAlias() { return this.alias; }
public boolean isRequired() { return this.required; }

public abstract String getRawPath();
public abstract Collection<String> getResolverInputs();
public abstract Descriptor resolve(Config data) throws ConfigLoadException;

@Override
public int compareTo(Descriptor o) {
int comp = getAlias().compareTo(o.getAlias());
if (comp != 0)
return comp;
comp = getProvider().compareTo(o.getProvider());
if (comp != 0)
return comp;
return getRawPath().compareTo(o.getRawPath());
}

@Override
public boolean equals(Object o) {
if (o instanceof Descriptor) {
return equals((Descriptor)o);
}
return super.equals(o);
}

@Override
public int hashCode() { return toString().hashCode(); }

@Override
public String toString() {
return alias+"="+provider+"://"+getRawPath();
}

private boolean equals(Descriptor o) {
return provider.equals(o.provider) && alias.equals(o.alias) && getRawPath().equals(o.getRawPath());
}

}
32 changes: 32 additions & 0 deletions cagey-config-api/src/main/java/com/xpcagey/config/api/Element.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.xpcagey.config.api;

import java.time.Duration;
import java.time.Instant;
import java.util.function.BooleanSupplier;
import java.util.function.DoubleSupplier;
import java.util.function.LongSupplier;

/**
* Each Element is a single value inside of configuration. A series of casting
* functions is provided regardless of the source type; where casts are not
* supported the zero or empty value should be returned.
*/
public interface Element extends Comparable<Element>, BooleanSupplier, DoubleSupplier, LongSupplier {
/**
* If an element is sensitive it should not be logged or stored without encryption
* @return true if sensitive
*/
boolean isSensitive();

/**
* The key for this element relative to the root of its configuration
* @return the key
*/
String getKey();

Duration getAsDuration();
Instant getAsInstant();
String getAsString();

boolean hasEqualValue(Element other);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.xpcagey.config.api;

/**
* IllegalPathException should be thrown by a ConfigEngine if a requested path is malformed or unreachable. If
* a resource is missing from the indicated path but the path itself is available, the ConfigServiceProvider should
* return Optional.empty() instead of throwing an exception.
*/
public class IllegalPathException extends ConfigLoadException {
public final String path;
public final String rawPath;
public final String provider;
public IllegalPathException(String provider, String path, String rawPath, Throwable cause) {
super("Path ["+path+"] ("+rawPath+") is not supported for provider ["+provider+"]", cause);
this.provider = provider;
this.path = path;
this.rawPath = path;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.xpcagey.config.api;

import java.util.Arrays;
import java.util.Collection;

public class KeyedDescriptor extends Descriptor {
private static final String DELIMITER = "::";
private final String key;
private final String[] sources;

public KeyedDescriptor(String provider, String key, String alias, boolean required, String... sources) {
super(provider, alias, required);
this.sources = sources;
this.key = key;
}

@Override public String getRawPath() { return this.key + "@" + String.join(DELIMITER, sources); }
@Override public Collection<String> getResolverInputs() { return Arrays.asList(this.sources); }
@Override public Descriptor resolve(Config config) throws MissingResolverFieldException {
String path;
String provider = getProvider();
try {
path = config.getOrThrow(this.key).getAsString();
return new PathDescriptor(provider, path, getAlias(), isRequired());
} catch (MissingElementException e) {
throw new MissingResolverFieldException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.xpcagey.config.api;

public class MissingConfigException extends ConfigLoadException {
public final Descriptor desc;
public MissingConfigException(Descriptor desc) {
super("Unable to find configuration for ["+desc.toString()+"]");
this.desc = desc;
}
}
Loading

0 comments on commit 1c320c8

Please sign in to comment.