Skip to content

Commit

Permalink
Merge pull request #135 from kobil-systems/enhancement/hierarchical-s…
Browse files Browse the repository at this point in the history
…ystem-properties2

Enhancement: Hierarchical System Properties
  • Loading branch information
vietj authored May 27, 2021
2 parents cd67cb9 + db18310 commit 2a48aa9
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 118 deletions.
27 changes: 26 additions & 1 deletion vertx-config/src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,31 @@ You can also configure the `raw-data` attribute (`false` by default). If `raw-da
values are made, and you'll be able to get raw values using `config.getString(key)`. It is useful when manipulating
large integers.
Furthermore, there is the `hierarchical` attribute (`false` by default). If `hierarchical` is `true`, the system
properties will be parsed as a nested JSON object, using the dot-separated property name as the path in the JSON object.
Example:
[source, $lang]
----
{@link examples.ConfigExamples#sysHierarchical()}
----
....
java -Dserver.host=localhost -Dserver.port=8080 -jar your-application.jar
....
This will read the system properties as JSON object equivalent to
[source,json]
----
{
"server": {
"host": "localhost",
"port": 8080
}
}
----
=== HTTP
This configuration store retrieves the configuration from an HTTP location. It can use
Expand Down Expand Up @@ -320,7 +345,7 @@ Get values:
[source, $lang]
----
{@link examples.ConfigExamples#propsWitHierarchicalStructure()}
{@link examples.ConfigExamples#propsWithHierarchicalStructure()}
----
== Listening for configuration changes
Expand Down
20 changes: 13 additions & 7 deletions vertx-config/src/main/java/examples/ConfigExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,24 +101,30 @@ public void json() {
}

public void sys() {
ConfigStoreOptions json = new ConfigStoreOptions()
ConfigStoreOptions sys = new ConfigStoreOptions()
.setType("sys")
.setConfig(new JsonObject().put("cache", false));
}

public void sysHierarchical() {
ConfigStoreOptions sysHierarchical = new ConfigStoreOptions()
.setType("sys")
.setConfig(new JsonObject().put("hierarchical", true));
}

public void env() {
ConfigStoreOptions json = new ConfigStoreOptions()
ConfigStoreOptions env = new ConfigStoreOptions()
.setType("env");
}

public void env2() {
ConfigStoreOptions json = new ConfigStoreOptions()
ConfigStoreOptions env = new ConfigStoreOptions()
.setType("env")
.setConfig(new JsonObject().put("raw-data", true));
}

public void env3() {
ConfigStoreOptions json = new ConfigStoreOptions()
ConfigStoreOptions env = new ConfigStoreOptions()
.setType("env")
.setConfig(new JsonObject().put("keys", new JsonArray().add("SERVICE1_HOST").add("SERVICE2_HOST")));
}
Expand Down Expand Up @@ -180,14 +186,14 @@ public void propsWithRawData() {
);
}

public void propsWitHierarchicalStructure() {
ConfigStoreOptions propertyWitHierarchical = new ConfigStoreOptions()
public void propsWithHierarchicalStructure() {
ConfigStoreOptions propertyWithHierarchical = new ConfigStoreOptions()
.setFormat("properties")
.setType("file")
.setConfig(new JsonObject().put("path", "hierarchical.properties").put("hierarchical", true)
);
ConfigRetrieverOptions options = new ConfigRetrieverOptions()
.addStore(propertyWitHierarchical);
.addStore(propertyWithHierarchical);

ConfigRetriever configRetriever = ConfigRetriever.create(Vertx.vertx(), options);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,11 @@
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.stream.Stream;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toList;

/**
* Transforms properties to json.
Expand All @@ -43,19 +35,14 @@
*/
public class PropertiesConfigProcessor implements ConfigProcessor {

private static final PropertiesReader FLAT_READER = new FlatPropertiesReader();

private static final PropertiesReader HIERARCHICAL_READER = new HierarchicalPropertiesReader();

@Override
public String name() {
return "properties";
}

@Override
public Future<JsonObject> process(Vertx vertx, JsonObject configuration, Buffer input) {
Boolean hierarchicalData = configuration.getBoolean("hierarchical", false);
PropertiesReader reader = hierarchicalData ? HIERARCHICAL_READER : FLAT_READER;
final boolean hierarchicalData = configuration.getBoolean("hierarchical", false);
// lock the input config before entering the execute blocking to avoid
// access from 2 different threads (the called e.g.: the event loop) and
// the thread pool thread.
Expand All @@ -66,98 +53,17 @@ public Future<JsonObject> process(Vertx vertx, JsonObject configuration, Buffer
return vertx.executeBlocking(future -> {
byte[] bytes = input.getBytes();
try (ByteArrayInputStream stream = new ByteArrayInputStream(bytes)) {
JsonObject created = reader.readAsJson(rawData, stream);
future.complete(created);
JsonObject parsed = readAsJson(stream, rawData, hierarchicalData);
future.complete(parsed);
} catch (Exception e) {
future.fail(e);
}
});
}

private interface PropertiesReader {

JsonObject readAsJson(boolean rawData, InputStream stream) throws IOException;
}

private static class FlatPropertiesReader implements PropertiesReader {

@Override
public JsonObject readAsJson(boolean rawData, InputStream byteStream) throws IOException {
Properties properties = new Properties();
properties.load(byteStream);
return JsonObjectHelper.from(properties, rawData);
}
}

private static class HierarchicalPropertiesReader implements PropertiesReader {

@Override
public JsonObject readAsJson(boolean rawData, InputStream byteStream) throws IOException {
try (
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(byteStream));
Stream<String> stream = bufferedReader.lines()
) {
return toJson(stream);
}
}

private JsonObject toJson(Stream<String> stream) {
return stream
.filter(line -> {
line = line.trim();
return !line.isEmpty()
&& !line.startsWith("#")
&& !line.startsWith("!");
})
.map(line -> line.split("="))
.map(raw -> {
String property = raw[0].trim();
Object value = tryParse(raw[1].trim());
List<String> paths = asList(property.split("\\."));
if (paths.size() == 1) {
return new JsonObject().put(property, value);
}
JsonObject json = toJson(paths.subList(1, paths.size()), value);
return new JsonObject().put(paths.get(0), json);
})
.reduce((json, other) -> json.mergeIn(other, true))
.orElse(new JsonObject());

}

private JsonObject toJson(List<String> paths, Object value) {
if (paths.size() == 0) {
return new JsonObject();
}
if (paths.size() == 1) {
return new JsonObject().put(paths.get(0), value);
}
String path = paths.get(0);
JsonObject jsonValue = toJson(paths.subList(1, paths.size()), value);
return new JsonObject().put(path, jsonValue);
}

private Object tryParse(String raw) {
if (raw.contains(",")) {
return Stream.of(raw.split(","))
.map(this::tryParse)
.collect(collectingAndThen(toList(), JsonArray::new));
}
if ("true".equals(raw)) {
return true;
}
if ("false".equals(raw)) {
return false;
}
try {
return new BigInteger(raw);
} catch (NumberFormatException ignore) {
}
try {
return new BigDecimal(raw);
} catch (NumberFormatException ignore) {
}
return raw;
}
private static JsonObject readAsJson(InputStream stream, boolean rawData, boolean hierarchical) throws IOException {
Properties properties = new Properties();
properties.load(stream);
return JsonObjectHelper.from(properties, rawData, hierarchical);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,22 @@ public class SystemPropertiesConfigStore implements ConfigStore {
private final VertxInternal vertx;
private final boolean cache;
private final Boolean rawData;
private final Boolean hierarchical;

private AtomicReference<Buffer> cached = new AtomicReference<>();

public SystemPropertiesConfigStore(Vertx vertx, JsonObject configuration) {
this.vertx = (VertxInternal) vertx;
cache = configuration.getBoolean("cache", true);
rawData = configuration.getBoolean("raw-data", false);
hierarchical = configuration.getBoolean("hierarchical", false);
}

@Override
public Future<Buffer> get() {
Buffer value = cached.get();
if (value == null) {
value = JsonObjectHelper.from(System.getProperties(), rawData).toBuffer();
value = JsonObjectHelper.from(System.getProperties(), rawData, hierarchical).toBuffer();
if (cache) {
cached.set(value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

Expand All @@ -47,7 +49,7 @@ public static void put(JsonObject json, String name, String value, boolean rawDa
json.put(name, rawData ? value : convert(value));
}

public static Object convert(String value) {
public static Object convert(String value) {
Objects.requireNonNull(value);

Boolean bool = asBoolean(value);
Expand Down Expand Up @@ -113,7 +115,11 @@ private static JsonArray asJsonArray(String s) {
} catch (Exception e) {
return null;
}
} else if (!s.startsWith("[") && !s.endsWith("]") && s.contains(",")) {
// Allow comma-separated syntax
return asJsonArray("[" + s + "]");
}

return null;
}

Expand All @@ -122,9 +128,36 @@ public static JsonObject from(Properties props) {
}

public static JsonObject from(Properties props, boolean rawData) {
JsonObject json = new JsonObject();
props.stringPropertyNames()
.forEach(name -> put(json, name, props.getProperty(name), rawData));
return json;
return from(props, rawData, false);
}

public static JsonObject from(Properties props, boolean rawData, boolean hierarchical) {
if (!hierarchical) {
JsonObject json = new JsonObject();
props.stringPropertyNames()
.forEach(name -> put(json, name, props.getProperty(name), rawData));
return json;
} else {
return props.stringPropertyNames()
.stream()
.map(path -> toJson(Arrays.asList(path.split("\\.")), props.getProperty(path), rawData))
.reduce((json, other) -> json.mergeIn(other, true))
.orElse(new JsonObject());
}
}

public static JsonObject toJson(List<String> paths, String value, boolean rawData) {
if (paths.size() == 0) {
return new JsonObject();
}

if (paths.size() == 1) {
JsonObject json = new JsonObject();
put(json, paths.get(0), value, rawData);

return json;
}

return new JsonObject().put(paths.get(0), toJson(paths.subList(1, paths.size()), value, rawData));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ public void init() {
System.setProperty("float", "25.3");
System.setProperty("true", "true");
System.setProperty("false", "false");

System.setProperty("hierarchical.key", "value");
System.setProperty("hierarchical.sub.foo", "bar");
System.setProperty("hierarchical.array", "[1, 2, 3]");
System.setProperty("hierarchical.int", "5");
System.setProperty("hierarchical.float", "25.3");
System.setProperty("hierarchical.true", "true");
System.setProperty("hierarchical.false", "false");
}

@After
Expand Down Expand Up @@ -68,6 +76,17 @@ public void testLoadingFromSystemProperties(TestContext context) {
getJsonConfiguration(vertx, store, ar -> {
ConfigChecker.check(ar);

// Check that properties were loaded to a flat object (hierarchical = false)
// At this point we know that the ar is successful (otherwise ConfigChecker.check would have failed)
JsonObject json = ar.result();
assertThat(json.getString("hierarchical.key")).isEqualTo("value");
assertThat(json.getString("hierarchical.sub.foo")).isEqualTo("bar");
assertThat(json.getJsonArray("hierarchical.array")).containsExactly(1, 2, 3);
assertThat(json.getInteger("hierarchical.int")).isEqualTo(5);
assertThat(json.getDouble("hierarchical.float")).isEqualTo(25.3);
assertThat(json.getBoolean("true")).isTrue();
assertThat(json.getBoolean("false")).isFalse();

// By default, the configuration is cached, try adding some entries
System.setProperty("new", "some new value");
getJsonConfiguration(vertx, store, ar2 -> {
Expand All @@ -78,6 +97,20 @@ public void testLoadingFromSystemProperties(TestContext context) {
});
}

@Test
public void testHierarchicalLoadingFromSystemProperties(TestContext context) {
Async async = context.async();
store = factory.create(vertx, new JsonObject().put("hierarchical", true));
getJsonConfiguration(vertx, store, ar -> {
// Check that the flat keys are still present
ConfigChecker.check(ar);

// Check that the "hierarchical" key was mapped to a deep JSON object
ConfigChecker.check(ar.map(json -> json.getJsonObject("hierarchical", new JsonObject())));
async.complete();
});
}

@Test
public void testLoadingFromSystemPropertiesWithoutCache(TestContext context) {
Async async = context.async();
Expand Down

0 comments on commit 2a48aa9

Please sign in to comment.