This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
// SPDX-FileCopyrightText: The openTCS Authors
|
||||
// SPDX-License-Identifier: MIT
|
||||
package org.opentcs.configuration.gestalt;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.opentcs.util.Assertions.checkState;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ServiceLoader;
|
||||
import org.github.gestalt.config.Gestalt;
|
||||
import org.github.gestalt.config.builder.GestaltBuilder;
|
||||
import org.github.gestalt.config.decoder.ProxyDecoderMode;
|
||||
import org.github.gestalt.config.entity.GestaltConfig;
|
||||
import org.github.gestalt.config.exceptions.GestaltException;
|
||||
import org.github.gestalt.config.reload.TimedConfigReloadStrategy;
|
||||
import org.github.gestalt.config.source.ConfigSource;
|
||||
import org.github.gestalt.config.source.ConfigSourcePackage;
|
||||
import org.github.gestalt.config.source.FileConfigSourceBuilder;
|
||||
import org.opentcs.configuration.ConfigurationBindingProvider;
|
||||
import org.opentcs.configuration.ConfigurationException;
|
||||
import org.opentcs.configuration.gestalt.decoders.ClassPathDecoder;
|
||||
import org.opentcs.configuration.gestalt.decoders.MapLiteralDecoder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A configuration binding provider implementation using gestalt.
|
||||
*/
|
||||
public class GestaltConfigurationBindingProvider
|
||||
implements
|
||||
ConfigurationBindingProvider {
|
||||
|
||||
/**
|
||||
* This class's logger.
|
||||
*/
|
||||
private static final Logger LOG
|
||||
= LoggerFactory.getLogger(GestaltConfigurationBindingProvider.class);
|
||||
/**
|
||||
* The key of the (system) property containing the reload interval.
|
||||
*/
|
||||
private static final String PROPKEY_RELOAD_INTERVAL = "opentcs.configuration.reload.interval";
|
||||
/**
|
||||
* The default reload interval.
|
||||
*/
|
||||
private static final long DEFAULT_RELOAD_INTERVAL = 10000;
|
||||
/**
|
||||
* Default configuration file name.
|
||||
*/
|
||||
private final Path defaultsPath;
|
||||
/**
|
||||
* Supplementary configuration files.
|
||||
*/
|
||||
private final Path[] supplementaryPaths;
|
||||
/**
|
||||
* The configuration entry point.
|
||||
*/
|
||||
private final Gestalt gestalt;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param defaultsPath Default configuration file name.
|
||||
* @param supplementaryPaths Supplementary configuration file names.
|
||||
*/
|
||||
public GestaltConfigurationBindingProvider(Path defaultsPath, Path... supplementaryPaths) {
|
||||
this.defaultsPath = requireNonNull(defaultsPath, "defaultsPath");
|
||||
this.supplementaryPaths = requireNonNull(supplementaryPaths, "supplementaryPaths");
|
||||
|
||||
this.gestalt = buildGestalt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T get(String prefix, Class<T> type) {
|
||||
try {
|
||||
return gestalt.getConfig(prefix, type);
|
||||
}
|
||||
catch (GestaltException e) {
|
||||
throw new ConfigurationException(
|
||||
String.format("Cannot get configuration value for prefix: '%s'", prefix),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Gestalt buildGestalt() {
|
||||
GestaltConfig gestaltConfig = new GestaltConfig();
|
||||
gestaltConfig.setTreatMissingValuesAsErrors(true);
|
||||
gestaltConfig.setProxyDecoderMode(ProxyDecoderMode.PASSTHROUGH);
|
||||
|
||||
try {
|
||||
Gestalt provider = new GestaltBuilder()
|
||||
.setGestaltConfig(gestaltConfig)
|
||||
.useCacheDecorator(true)
|
||||
.addDefaultDecoders()
|
||||
.addDecoder(new ClassPathDecoder())
|
||||
.addDecoder(new MapLiteralDecoder())
|
||||
.addSources(buildSources())
|
||||
.build();
|
||||
provider.loadConfigs();
|
||||
|
||||
return provider;
|
||||
}
|
||||
catch (GestaltException e) {
|
||||
throw new ConfigurationException(
|
||||
"An error occured while creating gestalt configuration binding provider",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ConfigSourcePackage> buildSources()
|
||||
throws GestaltException {
|
||||
Duration reloadInterval = reloadInterval();
|
||||
List<ConfigSourcePackage> sources = new ArrayList<>();
|
||||
|
||||
// A file for baseline defaults MUST exist in the distribution.
|
||||
checkState(
|
||||
defaultsPath.toFile().isFile(),
|
||||
"Required default configuration file {} does not exist.",
|
||||
defaultsPath.toFile().getAbsolutePath()
|
||||
);
|
||||
LOG.info(
|
||||
"Using default configuration file {}...",
|
||||
defaultsPath.toFile().getAbsolutePath()
|
||||
);
|
||||
sources.add(
|
||||
FileConfigSourceBuilder.builder()
|
||||
.setPath(defaultsPath)
|
||||
.addConfigReloadStrategy(new TimedConfigReloadStrategy(reloadInterval))
|
||||
.build()
|
||||
);
|
||||
|
||||
// Files with supplementary configuration MAY exist in the distribution.
|
||||
for (Path supplementaryPath : supplementaryPaths) {
|
||||
if (supplementaryPath.toFile().isFile()) {
|
||||
LOG.info(
|
||||
"Using overrides from supplementary configuration file {}...",
|
||||
supplementaryPath.toFile().getAbsolutePath()
|
||||
);
|
||||
sources.add(
|
||||
FileConfigSourceBuilder.builder()
|
||||
.setPath(supplementaryPath)
|
||||
.addConfigReloadStrategy(new TimedConfigReloadStrategy(reloadInterval))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
else {
|
||||
LOG.warn(
|
||||
"Supplementary configuration file {} not found, skipped.",
|
||||
supplementaryPath.toFile().getAbsolutePath()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (ConfigSource source : ServiceLoader.load(SupplementaryConfigSource.class)) {
|
||||
LOG.info(
|
||||
"Using overrides from additional configuration source implementation {}...",
|
||||
source.getClass()
|
||||
);
|
||||
sources.add(
|
||||
new ConfigSourcePackage(
|
||||
source,
|
||||
List.of(new TimedConfigReloadStrategy(reloadInterval))
|
||||
)
|
||||
);
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
private Duration reloadInterval() {
|
||||
String valueString = System.getProperty(PROPKEY_RELOAD_INTERVAL);
|
||||
|
||||
if (valueString == null) {
|
||||
LOG.info("Using default configuration reload interval ({} ms).", DEFAULT_RELOAD_INTERVAL);
|
||||
return Duration.ofMillis(DEFAULT_RELOAD_INTERVAL);
|
||||
}
|
||||
|
||||
try {
|
||||
long value = Long.parseLong(valueString);
|
||||
LOG.info("Using configuration reload interval of {} ms.", value);
|
||||
return Duration.ofMillis(value);
|
||||
}
|
||||
catch (NumberFormatException exc) {
|
||||
LOG.warn(
|
||||
"Could not parse '{}', using default configuration reload interval ({} ms).",
|
||||
valueString,
|
||||
DEFAULT_RELOAD_INTERVAL,
|
||||
exc
|
||||
);
|
||||
return Duration.ofMillis(DEFAULT_RELOAD_INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: The openTCS Authors
|
||||
// SPDX-License-Identifier: MIT
|
||||
package org.opentcs.configuration.gestalt;
|
||||
|
||||
import org.github.gestalt.config.source.ConfigSource;
|
||||
|
||||
/**
|
||||
* A supplementary source providing configuration overrides.
|
||||
*/
|
||||
public interface SupplementaryConfigSource
|
||||
extends
|
||||
ConfigSource {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// SPDX-FileCopyrightText: The openTCS Authors
|
||||
// SPDX-License-Identifier: MIT
|
||||
package org.opentcs.configuration.gestalt.decoders;
|
||||
|
||||
import org.github.gestalt.config.decoder.Decoder;
|
||||
import org.github.gestalt.config.decoder.DecoderContext;
|
||||
import org.github.gestalt.config.decoder.Priority;
|
||||
import org.github.gestalt.config.entity.ValidationError;
|
||||
import org.github.gestalt.config.entity.ValidationLevel;
|
||||
import org.github.gestalt.config.node.ConfigNode;
|
||||
import org.github.gestalt.config.node.NodeType;
|
||||
import org.github.gestalt.config.reflect.TypeCapture;
|
||||
import org.github.gestalt.config.tag.Tags;
|
||||
import org.github.gestalt.config.utils.ValidateOf;
|
||||
|
||||
/**
|
||||
* A decoder to decode fully qualified class names to their representative class object.
|
||||
*
|
||||
* This decoder looks through the class path to find a class with the specified class name
|
||||
* and returns the class object. It will fail if the specified class cannot be found or
|
||||
* the specified class cannot be assigned to the type that is expected to be returned.
|
||||
*/
|
||||
public class ClassPathDecoder
|
||||
implements
|
||||
Decoder<Class<?>> {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*/
|
||||
public ClassPathDecoder() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Priority priority() {
|
||||
return Priority.MEDIUM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return ClassPathDecoder.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDecode(String string, Tags tags, ConfigNode cn, TypeCapture<?> tc) {
|
||||
return Class.class.isAssignableFrom(tc.getRawType()) && tc.hasParameter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidateOf<Class<?>> decode(
|
||||
String path,
|
||||
Tags tags,
|
||||
ConfigNode node,
|
||||
TypeCapture<?> type,
|
||||
DecoderContext context
|
||||
) {
|
||||
// This decoder only decodes nodes of type leaf. For other types the default decoders
|
||||
// `ArrayDecoder` and `ObjectDecoder` will eventually call this decoder if necessary.
|
||||
if (node.getNodeType() != NodeType.LEAF) {
|
||||
return ValidateOf.inValid(
|
||||
new ValidationError.DecodingExpectedLeafNodeType(path, node, this.name())
|
||||
);
|
||||
}
|
||||
// Look for a class with the configured name. The class must be assignable to the
|
||||
// class this decoder is expected to return via the type capture.
|
||||
return node.getValue().map(className -> {
|
||||
try {
|
||||
Class<?> configuredClass = Class.forName(className);
|
||||
if (type.getFirstParameterType().isAssignableFrom(configuredClass)) {
|
||||
return ValidateOf.<Class<?>>valid(configuredClass);
|
||||
}
|
||||
else {
|
||||
return ValidateOf.<Class<?>>inValid(
|
||||
new CannotCast(className, type.getFirstParameterType().getName())
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
return ValidateOf.<Class<?>>inValid(new ClassNotFound(className));
|
||||
}
|
||||
}).orElse(
|
||||
ValidateOf.<Class<?>>inValid(
|
||||
new ValidationError.DecodingLeafMissingValue(path, this.name())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The configured class cannot be cast to the class expected by the decoder.
|
||||
*/
|
||||
public static class CannotCast
|
||||
extends
|
||||
ValidationError {
|
||||
|
||||
private final String from;
|
||||
private final String to;
|
||||
|
||||
public CannotCast(String from, String to) {
|
||||
super(ValidationLevel.ERROR);
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "The class `" + this.from + "` cannot be cast to `" + this.to + "`.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The configured class cannot be found in the class path.
|
||||
*/
|
||||
public static class ClassNotFound
|
||||
extends
|
||||
ValidationError {
|
||||
|
||||
private final String className;
|
||||
|
||||
public ClassNotFound(String className) {
|
||||
super(ValidationLevel.ERROR);
|
||||
this.className = className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "The class `" + this.className + "` cannot be found.";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// SPDX-FileCopyrightText: The openTCS Authors
|
||||
// SPDX-License-Identifier: MIT
|
||||
package org.opentcs.configuration.gestalt.decoders;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.github.gestalt.config.decoder.Decoder;
|
||||
import org.github.gestalt.config.decoder.DecoderContext;
|
||||
import org.github.gestalt.config.decoder.Priority;
|
||||
import org.github.gestalt.config.entity.ValidationError;
|
||||
import org.github.gestalt.config.entity.ValidationLevel;
|
||||
import org.github.gestalt.config.node.ConfigNode;
|
||||
import org.github.gestalt.config.node.LeafNode;
|
||||
import org.github.gestalt.config.node.NodeType;
|
||||
import org.github.gestalt.config.reflect.TypeCapture;
|
||||
import org.github.gestalt.config.tag.Tags;
|
||||
import org.github.gestalt.config.utils.ValidateOf;
|
||||
|
||||
/**
|
||||
* A decoder to read map literals in the form
|
||||
* {@code <KEY_1>=<VALUE_1>,<KEY_2>=<VALUE_2>,...,<KEY_N>=<VALUE_N>}, where the key-value pairs
|
||||
* (i.e. map entries) are separated by commas as a delimiter.
|
||||
*/
|
||||
public class MapLiteralDecoder
|
||||
implements
|
||||
Decoder<Map<?, ?>> {
|
||||
|
||||
public MapLiteralDecoder() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Priority priority() {
|
||||
return Priority.HIGH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return MapLiteralDecoder.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDecode(String path, Tags tags, ConfigNode node, TypeCapture<?> type) {
|
||||
return node.getNodeType() == NodeType.LEAF
|
||||
&& Map.class.isAssignableFrom(type.getRawType())
|
||||
&& type.getParameterTypes().size() == 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidateOf<Map<?, ?>> decode(
|
||||
String path,
|
||||
Tags tags,
|
||||
ConfigNode node,
|
||||
TypeCapture<?> type,
|
||||
DecoderContext decoderContext
|
||||
) {
|
||||
// This decoder only decodes nodes of type leaf. For other types the default decoders
|
||||
// `ArrayDecoder` and `ObjectDecoder` will eventually call this decoder if necessary.
|
||||
if (node.getNodeType() != NodeType.LEAF) {
|
||||
return ValidateOf.inValid(
|
||||
new ValidationError.DecodingExpectedLeafNodeType(path, node, this.name())
|
||||
);
|
||||
}
|
||||
|
||||
if (node.getValue().isEmpty()) {
|
||||
return ValidateOf.inValid(
|
||||
new ValidationError.LeafNodesHaveNoValues(path)
|
||||
);
|
||||
}
|
||||
|
||||
List<ValidationError> errors = new ArrayList<>();
|
||||
Map<Object, Object> result = new HashMap<>();
|
||||
|
||||
// Split the node value on ',' to seperate it into `key=value` pairs and split those
|
||||
// again into the `key` and `value`. Then decode the key and value to the required types.
|
||||
for (String entry : node.getValue().get().split(",")) {
|
||||
if (entry.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] keyValuePair = entry.split("=");
|
||||
if (keyValuePair.length != 2) {
|
||||
errors.add(new MapEntryFormatInvalid(entry));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decode the key string to the required key type.
|
||||
ValidateOf<?> key = decoderContext.getDecoderService()
|
||||
.decodeNode(
|
||||
path,
|
||||
tags,
|
||||
new LeafNode(keyValuePair[0].trim()),
|
||||
type.getFirstParameterType(),
|
||||
decoderContext
|
||||
);
|
||||
if (key.hasErrors()) {
|
||||
errors.addAll(key.getErrors());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decode the value string to the required value type.
|
||||
ValidateOf<?> value = decoderContext.getDecoderService()
|
||||
.decodeNode(
|
||||
path,
|
||||
tags,
|
||||
new LeafNode(keyValuePair[1].trim()),
|
||||
type.getSecondParameterType(),
|
||||
decoderContext
|
||||
);
|
||||
if (value.hasErrors()) {
|
||||
errors.addAll(value.getErrors());
|
||||
continue;
|
||||
}
|
||||
|
||||
result.put(key.results(), value.results());
|
||||
}
|
||||
|
||||
return ValidateOf.validateOf(result, errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* A validation error for map entries not in the format {@code <KEY>=<VALUE>}.
|
||||
*/
|
||||
public static class MapEntryFormatInvalid
|
||||
extends
|
||||
ValidationError {
|
||||
|
||||
private final String rawEntry;
|
||||
|
||||
public MapEntryFormatInvalid(String rawEntry) {
|
||||
super(ValidationLevel.ERROR);
|
||||
this.rawEntry = rawEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Map entry is not in the format '<KEY>=<VALUE>':" + rawEntry;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-FileCopyrightText: The openTCS Authors
|
||||
// SPDX-License-Identifier: MIT
|
||||
package org.opentcs.configuration.gestalt;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A dummy class for testing sample configuration entries.
|
||||
*/
|
||||
public class DummyClass {
|
||||
|
||||
private final String name;
|
||||
private final String surname;
|
||||
private final int age;
|
||||
|
||||
public DummyClass() {
|
||||
name = "";
|
||||
surname = "";
|
||||
age = 0;
|
||||
}
|
||||
|
||||
public DummyClass(String paramString) {
|
||||
String[] split = paramString.split("\\|", 3);
|
||||
name = split[0];
|
||||
surname = split[1];
|
||||
age = Integer.parseInt(split[2]);
|
||||
}
|
||||
|
||||
public DummyClass(String name, String surname, int age) {
|
||||
this.name = name;
|
||||
this.surname = surname;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getSurname() {
|
||||
return surname;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof DummyClass)) {
|
||||
return false;
|
||||
}
|
||||
DummyClass other = (DummyClass) o;
|
||||
return name.equals(other.name) && surname.equals(other.surname) && age == other.age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 7;
|
||||
hash = 23 * hash + Objects.hashCode(this.name);
|
||||
hash = 23 * hash + Objects.hashCode(this.surname);
|
||||
hash = 23 * hash + this.age;
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName() + " - " + getSurname() + ":" + getAge();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// SPDX-FileCopyrightText: The openTCS Authors
|
||||
// SPDX-License-Identifier: MIT
|
||||
package org.opentcs.configuration.gestalt;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opentcs.configuration.ConfigurationBindingProvider;
|
||||
|
||||
/**
|
||||
* Tests for reading configuration entries with gestalt.
|
||||
*/
|
||||
public class SampleConfigurationTest {
|
||||
|
||||
private ConfigurationBindingProvider input;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
input = gestaltConfigurationBindingProvider();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBoolean() {
|
||||
SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class);
|
||||
assertThat(config.simpleBoolean(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInteger() {
|
||||
SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class);
|
||||
assertThat(config.simpleInteger(), is(600));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testString() {
|
||||
SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class);
|
||||
assertThat(config.simpleString(), is("HelloWorld"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnum() {
|
||||
SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class);
|
||||
assertThat(config.simpleEnum(), is(SampleConfigurationTest.DummyEnum.ORDER));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringList() {
|
||||
SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class);
|
||||
assertThat(config.stringList(), equalTo(List.of("A", "B", "C")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringMap() {
|
||||
SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class);
|
||||
assertThat(config.stringMap(), equalTo(Map.of("A", "1", "B", "2", "C", "3")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnumMap() {
|
||||
SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class);
|
||||
assertThat(config.enumMap(), equalTo(Map.of(DummyEnum.ORDER, "1", DummyEnum.POINT, "2")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testObjectList() {
|
||||
SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class);
|
||||
assertThat(
|
||||
config.objectList(), equalTo(
|
||||
List.of(
|
||||
new DummyClass("A", "B", 1),
|
||||
new DummyClass("C", "D", 2)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringConstructor() {
|
||||
SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class);
|
||||
assertThat(config.stringConstructor(), equalTo(new DummyClass("A", "B", 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClassPath() {
|
||||
SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class);
|
||||
assertThat(config.classPath(), is(DummyClass.class));
|
||||
}
|
||||
|
||||
private static ConfigurationBindingProvider gestaltConfigurationBindingProvider() {
|
||||
try {
|
||||
return new GestaltConfigurationBindingProvider(
|
||||
Paths.get(
|
||||
Thread.currentThread().getContextClassLoader()
|
||||
.getResource("org/opentcs/configuration/gestalt/sampleConfig.properties").toURI()
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
Logger.getLogger(SampleConfigurationTest.class.getName()).log(Level.SEVERE, null, ex);
|
||||
assertFalse(true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface SampleConfig {
|
||||
|
||||
/**
|
||||
* This configuration's prefix.
|
||||
*/
|
||||
String PREFIX = "sampleConfig";
|
||||
|
||||
boolean simpleBoolean();
|
||||
|
||||
int simpleInteger();
|
||||
|
||||
String simpleString();
|
||||
|
||||
SampleConfigurationTest.DummyEnum simpleEnum();
|
||||
|
||||
List<String> stringList();
|
||||
|
||||
Map<String, String> stringMap();
|
||||
|
||||
Map<DummyEnum, String> enumMap();
|
||||
|
||||
List<DummyClass> objectList();
|
||||
|
||||
DummyClass stringConstructor();
|
||||
|
||||
Class<DummyClass> classPath();
|
||||
|
||||
}
|
||||
|
||||
public enum DummyEnum {
|
||||
/**
|
||||
* Vehicle.
|
||||
*/
|
||||
VEHICLE,
|
||||
/**
|
||||
* Point.
|
||||
*/
|
||||
POINT,
|
||||
/**
|
||||
* Order.
|
||||
*/
|
||||
ORDER
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// SPDX-FileCopyrightText: The openTCS Authors
|
||||
// SPDX-License-Identifier: MIT
|
||||
package org.opentcs.configuration.gestalt.decoders;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.github.gestalt.config.Gestalt;
|
||||
import org.github.gestalt.config.builder.GestaltBuilder;
|
||||
import org.github.gestalt.config.decoder.DecoderContext;
|
||||
import org.github.gestalt.config.decoder.DecoderRegistry;
|
||||
import org.github.gestalt.config.decoder.DecoderService;
|
||||
import org.github.gestalt.config.exceptions.GestaltConfigurationException;
|
||||
import org.github.gestalt.config.exceptions.GestaltException;
|
||||
import org.github.gestalt.config.lexer.SentenceLexer;
|
||||
import org.github.gestalt.config.node.ConfigNodeService;
|
||||
import org.github.gestalt.config.node.LeafNode;
|
||||
import org.github.gestalt.config.path.mapper.StandardPathMapper;
|
||||
import org.github.gestalt.config.reflect.TypeCapture;
|
||||
import org.github.gestalt.config.source.MapConfigSourceBuilder;
|
||||
import org.github.gestalt.config.tag.Tags;
|
||||
import org.github.gestalt.config.utils.ValidateOf;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests the map literal decoder {@link MapLiteralDecoder} for Gestalt.
|
||||
*/
|
||||
class MapLiteralDecoderTest {
|
||||
|
||||
private ConfigNodeService configNodeService;
|
||||
private SentenceLexer lexer;
|
||||
private DecoderService decoderService;
|
||||
|
||||
@BeforeEach
|
||||
void setup()
|
||||
throws GestaltConfigurationException {
|
||||
configNodeService = mock(ConfigNodeService.class);
|
||||
lexer = mock(SentenceLexer.class);
|
||||
decoderService = new DecoderRegistry(
|
||||
Collections.singletonList(new MapLiteralDecoder()),
|
||||
configNodeService,
|
||||
lexer,
|
||||
List.of(new StandardPathMapper())
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDecodeMapLiteral()
|
||||
throws GestaltException {
|
||||
Map<String, String> config = Map.of("entry_path", "AAAA=1, BBBB=2, CCCC=3");
|
||||
Gestalt gestalt = buildGestaltConfig(config);
|
||||
|
||||
Map<String, Integer> result = gestalt.getConfig(
|
||||
"entry_path",
|
||||
new TypeCapture<Map<String, Integer>>() {
|
||||
}
|
||||
);
|
||||
|
||||
assertEquals(result.get("AAAA"), 1);
|
||||
assertEquals(result.get("BBBB"), 2);
|
||||
assertEquals(result.get("CCCC"), 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDecodeMapLiteralToEnum()
|
||||
throws GestaltException {
|
||||
Map<String, String> config = Map.of("entry_path", "Foo=1, Bar=2, Baz=3");
|
||||
Gestalt gestalt = buildGestaltConfig(config);
|
||||
|
||||
Map<Things, Integer> result = gestalt.getConfig(
|
||||
"entry_path",
|
||||
new TypeCapture<Map<Things, Integer>>() {
|
||||
}
|
||||
);
|
||||
|
||||
assertEquals(result.get(Things.Foo), 1);
|
||||
assertEquals(result.get(Things.Bar), 2);
|
||||
assertEquals(result.get(Things.Baz), 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyLeafShouldGiveAnEmptyMap()
|
||||
throws GestaltException {
|
||||
Map<String, String> config = Map.of("entry_path", "");
|
||||
Gestalt gestalt = buildGestaltConfig(config);
|
||||
|
||||
Map<String, Integer> result = gestalt.getConfig(
|
||||
"entry_path",
|
||||
new TypeCapture<Map<String, Integer>>() {
|
||||
}
|
||||
);
|
||||
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGiveErrorWhenWrongDelimiterIsUsed() {
|
||||
MapLiteralDecoder decoder = new MapLiteralDecoder();
|
||||
|
||||
ValidateOf<Map<?, ?>> result = decoder.decode(
|
||||
"entry_path",
|
||||
Tags.of(),
|
||||
new LeafNode("AAAA=1; BBBB=2; CCCC=3"),
|
||||
new TypeCapture<Map<String, String>>() {
|
||||
},
|
||||
new DecoderContext(decoderService, null)
|
||||
);
|
||||
assertThat(result.getErrors(), hasSize(1));
|
||||
assertThat(
|
||||
result.getErrors().get(0),
|
||||
instanceOf(MapLiteralDecoder.MapEntryFormatInvalid.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGiveErrorWhenWrongAsignmentIsUsed() {
|
||||
MapLiteralDecoder decoder = new MapLiteralDecoder();
|
||||
|
||||
ValidateOf<Map<?, ?>> result = decoder.decode(
|
||||
"entry_path",
|
||||
Tags.of(),
|
||||
new LeafNode("AAAA~1, BBBB~2, CCCC~3"),
|
||||
new TypeCapture<Map<String, String>>() {
|
||||
},
|
||||
new DecoderContext(decoderService, null)
|
||||
);
|
||||
assertThat(result.getErrors(), hasSize(3));
|
||||
assertThat(
|
||||
result.getErrors().get(0),
|
||||
instanceOf(MapLiteralDecoder.MapEntryFormatInvalid.class)
|
||||
);
|
||||
assertThat(
|
||||
result.getErrors().get(1),
|
||||
instanceOf(MapLiteralDecoder.MapEntryFormatInvalid.class)
|
||||
);
|
||||
assertThat(
|
||||
result.getErrors().get(2),
|
||||
instanceOf(MapLiteralDecoder.MapEntryFormatInvalid.class)
|
||||
);
|
||||
}
|
||||
|
||||
private enum Things {
|
||||
Foo,
|
||||
Bar,
|
||||
Baz
|
||||
}
|
||||
|
||||
private Gestalt buildGestaltConfig(Map<String, String> config)
|
||||
throws GestaltException {
|
||||
Gestalt gestalt = new GestaltBuilder()
|
||||
.addDefaultDecoders()
|
||||
.addDecoder(new ClassPathDecoder())
|
||||
.addDecoder(new MapLiteralDecoder())
|
||||
.addSource(MapConfigSourceBuilder.builder().setCustomConfig(config).build())
|
||||
.build();
|
||||
gestalt.loadConfigs();
|
||||
return gestalt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# SPDX-FileCopyrightText: The openTCS Authors
|
||||
# SPDX-License-Identifier: CC-BY-4.0
|
||||
|
||||
sampleConfig.simpleBoolean = true
|
||||
sampleConfig.simpleInteger = 600
|
||||
sampleConfig.simpleString = HelloWorld
|
||||
sampleConfig.simpleEnum= ORDER
|
||||
sampleConfig.stringList = A,B,C
|
||||
sampleConfig.stringMap = A=1,B=2,C=3
|
||||
sampleConfig.enumMap = ORDER=1,POINT=2
|
||||
sampleConfig.objectList = A|B|1,C|D|2
|
||||
sampleConfig.stringConstructor = A|B|1
|
||||
sampleConfig.classPath = org.opentcs.configuration.gestalt.DummyClass
|
||||
Reference in New Issue
Block a user