From 9aa53a87629e50c7bba5a5be889fd2c8dad36286 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Mon, 30 Mar 2020 16:48:07 +0100 Subject: [PATCH] #59 - Added a ConfigValueConfigSource to trace the origin of a configuration key. --- .../pages/config-sources/config-sources.adoc | 3 + .../configvalueproperties-config-source.adoc | 14 + .../java/io/smallrye/config/ConfigValue.java | 53 ++- .../config/ConfigValueConfigSource.java | 67 ++++ .../smallrye/config/ConfigValueMapView.java | 123 +++++++ .../config/ConfigValueProperties.java | 305 ++++++++++++++++++ .../ConfigValuePropertiesConfigSource.java | 34 ++ .../MapBackedConfigValueConfigSource.java | 61 ++++ .../io/smallrye/config/SmallRyeConfig.java | 24 +- .../config/ConfigValueMapViewTest.java | 125 +++++++ ...ConfigValuePropertiesConfigSourceTest.java | 32 ++ .../config/ConfigValuePropertiesTest.java | 80 +++++ .../test/resources/config-values.properties | 20 ++ 13 files changed, 932 insertions(+), 9 deletions(-) create mode 100644 doc/modules/ROOT/pages/config-sources/configvalueproperties-config-source.adoc create mode 100644 implementation/src/main/java/io/smallrye/config/ConfigValueConfigSource.java create mode 100644 implementation/src/main/java/io/smallrye/config/ConfigValueMapView.java create mode 100644 implementation/src/main/java/io/smallrye/config/ConfigValueProperties.java create mode 100644 implementation/src/main/java/io/smallrye/config/ConfigValuePropertiesConfigSource.java create mode 100644 implementation/src/main/java/io/smallrye/config/MapBackedConfigValueConfigSource.java create mode 100644 implementation/src/test/java/io/smallrye/config/ConfigValueMapViewTest.java create mode 100644 implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesConfigSourceTest.java create mode 100644 implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesTest.java create mode 100644 implementation/src/test/resources/config-values.properties diff --git a/doc/modules/ROOT/pages/config-sources/config-sources.adoc b/doc/modules/ROOT/pages/config-sources/config-sources.adoc index d28ef8aaf..39482df91 100644 --- a/doc/modules/ROOT/pages/config-sources/config-sources.adoc +++ b/doc/modules/ROOT/pages/config-sources/config-sources.adoc @@ -9,6 +9,7 @@ In addition to the default Config Sources specified by MicroProfile Config (Envi and `microprofile-config.properties` file), SmallRye Config provides the following additional Sources: * <> +* <> * <> * <> * <> @@ -16,6 +17,8 @@ and `microprofile-config.properties` file), SmallRye Config provides the followi include::properties-config-source.adoc[] +include::configvalueproperties-config-source.adoc[] + include::filesystem-config-source.adoc[] include::hocon-config-source.adoc[] diff --git a/doc/modules/ROOT/pages/config-sources/configvalueproperties-config-source.adoc b/doc/modules/ROOT/pages/config-sources/configvalueproperties-config-source.adoc new file mode 100644 index 000000000..d842db3c6 --- /dev/null +++ b/doc/modules/ROOT/pages/config-sources/configvalueproperties-config-source.adoc @@ -0,0 +1,14 @@ +[[configvalueproperties-config-source]] +== Config Value Properties Config Source + +Creates a Config Source with `ConfigValue` support from a properties file (referenced by its URL). + +The `ConfigValue` is a metadata object, containing additional information to each configuration. This includes the +Config Source origin, ordinal, or the line number from where the Config was loaded. This is useful for debugging +information. + +=== Usage + +This Config Source is not automatically registered. This means that need to provide your own +`ConfigSourceProvider` implementation and registration via `ServiceLoader` to use this Config Source like documented in +MicroProfile Config `ConfigSource` specification. diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValue.java b/implementation/src/main/java/io/smallrye/config/ConfigValue.java index 0b67f35c3..138719f26 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigValue.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigValue.java @@ -1,16 +1,33 @@ package io.smallrye.config; +import java.util.Objects; + +/** + * The ConfigValue is a metadata object that holds additional information after the lookup of a configuration. + *

+ * + * Right now, it is able to hold information like the configuration name, value, the Config Source from where + * the configuration was loaded, the ordinal of the Config Source and a line number from where the configuration was + * read if exists. + *

+ * + * This is used together with {@link ConfigValueConfigSource} and {@link ConfigSourceInterceptor} to expose the + * Configuration lookup metadata. + */ public class ConfigValue { private final String name; private final String value; private final String configSourceName; private final int configSourceOrdinal; + private final int lineNumber; + private ConfigValue(final ConfigValueBuilder builder) { this.name = builder.name; this.value = builder.value; this.configSourceName = builder.configSourceName; this.configSourceOrdinal = builder.configSourceOrdinal; + this.lineNumber = builder.lineNumber; } public String getName() { @@ -29,6 +46,10 @@ public int getConfigSourceOrdinal() { return configSourceOrdinal; } + public int getLineNumber() { + return lineNumber; + } + public ConfigValue withName(final String name) { return from().withName(name).build(); } @@ -45,12 +66,36 @@ public ConfigValue withConfigSourceOrdinal(final int configSourceOrdinal) { return from().withConfigSourceOrdinal(configSourceOrdinal).build(); } + public ConfigValue withLineNumber(final int lineNumber) { + return from().withLineNumber(lineNumber).build(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ConfigValue that = (ConfigValue) o; + return name.equals(that.name) && + value.equals(that.value) && + configSourceName.equals(that.configSourceName); + } + + @Override + public int hashCode() { + return Objects.hash(name, value, configSourceName); + } + ConfigValueBuilder from() { return new ConfigValueBuilder() .withName(name) .withValue(value) .withConfigSourceName(configSourceName) - .withConfigSourceOrdinal(configSourceOrdinal); + .withConfigSourceOrdinal(configSourceOrdinal) + .withLineNumber(lineNumber); } public static ConfigValueBuilder builder() { @@ -62,6 +107,7 @@ public static class ConfigValueBuilder { private String value; private String configSourceName; private int configSourceOrdinal; + private int lineNumber = -1; public ConfigValueBuilder withName(final String name) { this.name = name; @@ -83,6 +129,11 @@ public ConfigValueBuilder withConfigSourceOrdinal(final int configSourceOrdinal) return this; } + public ConfigValueBuilder withLineNumber(final int lineNumber) { + this.lineNumber = lineNumber; + return this; + } + public ConfigValue build() { return new ConfigValue(this); } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValueConfigSource.java b/implementation/src/main/java/io/smallrye/config/ConfigValueConfigSource.java new file mode 100644 index 000000000..88bf17a57 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigValueConfigSource.java @@ -0,0 +1,67 @@ +package io.smallrye.config; + +import java.util.Collections; +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +/** + * Extends the original {@link ConfigSource} to expose methods that return a {@link ConfigValue}. The + * {@link ConfigValue} allows to retrieve additional metadata associated with the configuration resolution. + *

+ * + * This is to work around the limitation from the original {@link ConfigSource}. It exposes everything as plain Strings + * and it is not possible to retrieve additional information associated with the Configuration. The + * ConfigValueConfigSource tries to make this possible. + *

+ * + * Ideally, this should move the the MicroProfile Config API, once the concept is well-proven. + */ +public interface ConfigValueConfigSource extends ConfigSource { + /** + * Return the {@link ConfigValue} for the specified property in this configuration source. + * + * @param propertyName the property name + * @return the ConfigValue, or {@code null} if the property is not present + */ + ConfigValue getConfigValue(String propertyName); + + /** + * Return the properties in this configuration source as a Map of String and {@link ConfigValue}. + * + * @return a map containing properties of this configuration source + */ + Map getConfigValueProperties(); + + /** + * Return the properties in this configuration source as a map. + *

+ * + * This wraps the original {@link ConfigValue} map returned by + * {@link ConfigValueConfigSource#getConfigValueProperties()} and provides a view over the original map + * via {@link ConfigValueMapView}. + * + * @return a map containing properties of this configuration source + */ + @Override + default Map getProperties() { + return Collections.unmodifiableMap(new ConfigValueMapView(getConfigValueProperties())); + } + + /** + * Return the value for the specified property in this configuration source. + *

+ * + * This wraps the original {@link ConfigValue} returned by {@link ConfigValueConfigSource#getConfigValue(String)} + * and unwraps the property value contained {@link ConfigValue}. If the {@link ConfigValue} is null the unwrapped + * value and return is also null. + * + * @param propertyName the property name + * @return the property value, or {@code null} if the property is not present + */ + @Override + default String getValue(String propertyName) { + final ConfigValue value = getConfigValue(propertyName); + return value != null ? value.getValue() : null; + } +} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValueMapView.java b/implementation/src/main/java/io/smallrye/config/ConfigValueMapView.java new file mode 100644 index 000000000..90d45caac --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigValueMapView.java @@ -0,0 +1,123 @@ +package io.smallrye.config; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * The ConfigValueMapView is view over a Map of String configs names and ConfigValue value. + *

+ * + * Use this to wrap the ConfigValue map and expose it where a Map of String name and String value is required. + */ +public class ConfigValueMapView extends AbstractMap { + private final Map delegate; + + ConfigValueMapView(final Map delegate) { + this.delegate = Collections.unmodifiableMap(delegate); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(final Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(final Object value) { + return values().contains(value); + } + + @Override + public String get(final Object key) { + final ConfigValue configValue = delegate.get(key); + return configValue != null ? configValue.getValue() : null; + } + + private transient Set> entrySet; + private transient Collection values; + + @Override + public Set keySet() { + return delegate.keySet(); + } + + @Override + public Set> entrySet() { + if (entrySet == null) { + entrySet = new AbstractSet>() { + @Override + public Iterator> iterator() { + return new Iterator>() { + final Iterator> delegate = ConfigValueMapView.this.delegate.entrySet() + .iterator(); + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Entry next() { + final Entry next = delegate.next(); + final ConfigValue configValue = next.getValue(); + final String value = configValue != null ? configValue.getValue() : null; + return new AbstractMap.SimpleImmutableEntry<>(next.getKey(), value); + } + }; + } + + @Override + public int size() { + return delegate.size(); + } + }; + } + return entrySet; + } + + @Override + public Collection values() { + if (values == null) { + values = new AbstractCollection() { + @Override + public Iterator iterator() { + final Iterator delegate = ConfigValueMapView.this.delegate.values().iterator(); + + return new Iterator() { + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public String next() { + final ConfigValue configValue = delegate.next(); + return configValue != null ? configValue.getValue() : null; + } + }; + } + + @Override + public int size() { + return delegate.size(); + } + }; + } + return values; + } +} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValueProperties.java b/implementation/src/main/java/io/smallrye/config/ConfigValueProperties.java new file mode 100644 index 000000000..e0207833d --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigValueProperties.java @@ -0,0 +1,305 @@ +package io.smallrye.config; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.HashMap; + +/** + * Loads properties as {@link ConfigValue}. + *

+ * + * This class is mostly a subset copy of {@link java.util.Properties}. This was required to be able to keep track of + * the line number from which the configuration was loaded. + */ +class ConfigValueProperties extends HashMap { + private final String configSourceName; + private final int configSourceOrdinal; + + public ConfigValueProperties(final String configSourceName, final int configSourceOrdinal) { + this.configSourceName = configSourceName; + this.configSourceOrdinal = configSourceOrdinal; + } + + public synchronized void load(Reader reader) throws IOException { + load0(new LineReader(reader)); + } + + public synchronized void load(InputStream inStream) throws IOException { + load0(new LineReader(inStream)); + } + + private void load0(LineReader lr) throws IOException { + char[] convtBuf = new char[1024]; + int limit; + int keyLen; + int valueStart; + char c; + boolean hasSep; + boolean precedingBackslash; + + while ((limit = lr.readLine()) >= 0) { + c = 0; + keyLen = 0; + valueStart = limit; + hasSep = false; + + //System.out.println("line=<" + new String(lineBuf, 0, limit) + ">"); + precedingBackslash = false; + while (keyLen < limit) { + c = lr.lineBuf[keyLen]; + //need check if escaped. + if ((c == '=' || c == ':') && !precedingBackslash) { + valueStart = keyLen + 1; + hasSep = true; + break; + } else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) { + valueStart = keyLen + 1; + break; + } + if (c == '\\') { + precedingBackslash = !precedingBackslash; + } else { + precedingBackslash = false; + } + keyLen++; + } + while (valueStart < limit) { + c = lr.lineBuf[valueStart]; + if (c != ' ' && c != '\t' && c != '\f') { + if (!hasSep && (c == '=' || c == ':')) { + hasSep = true; + } else { + break; + } + } + valueStart++; + } + String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf); + String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf); + put(key, ConfigValue.builder() + .withName(key) + .withValue(value) + .withConfigSourceName(configSourceName) + .withConfigSourceOrdinal(configSourceOrdinal) + .withLineNumber(lr.lineNumber) + .build()); + } + } + + class LineReader { + public LineReader(InputStream inStream) { + this.inStream = inStream; + inByteBuf = new byte[8192]; + } + + public LineReader(Reader reader) { + this.reader = reader; + inCharBuf = new char[8192]; + } + + byte[] inByteBuf; + char[] inCharBuf; + char[] lineBuf = new char[1024]; + int inLimit = 0; + int inOff = 0; + InputStream inStream; + Reader reader; + int lineNumber = 0; + int addBackslash = 0; + + int readLine() throws IOException { + int len = 0; + char c = 0; + + boolean skipWhiteSpace = true; + boolean isCommentLine = false; + boolean isNewLine = true; + boolean appendedLineBegin = false; + boolean precedingBackslash = false; + boolean skipLF = false; + lineNumber = ++lineNumber + addBackslash; + addBackslash = 0; + + while (true) { + if (inOff >= inLimit) { + inLimit = (inStream == null) ? reader.read(inCharBuf) + : inStream.read(inByteBuf); + inOff = 0; + if (inLimit <= 0) { + if (len == 0 || isCommentLine) { + return -1; + } + if (precedingBackslash) { + len--; + } + return len; + } + } + if (inStream != null) { + //The line below is equivalent to calling a + //ISO8859-1 decoder. + c = (char) (0xff & inByteBuf[inOff++]); + } else { + c = inCharBuf[inOff++]; + } + if (skipLF) { + skipLF = false; + if (c == '\n') { + lineNumber++; + continue; + } + } + if (skipWhiteSpace) { + if (c == ' ' || c == '\t' || c == '\f') { + continue; + } + if (!appendedLineBegin && (c == '\r' || c == '\n')) { + lineNumber++; + continue; + } + skipWhiteSpace = false; + appendedLineBegin = false; + } + if (isNewLine) { + isNewLine = false; + if (c == '#' || c == '!') { + isCommentLine = true; + continue; + } + } + + if (c != '\n' && c != '\r') { + lineBuf[len++] = c; + if (len == lineBuf.length) { + int newLength = lineBuf.length * 2; + if (newLength < 0) { + newLength = Integer.MAX_VALUE; + } + char[] buf = new char[newLength]; + System.arraycopy(lineBuf, 0, buf, 0, lineBuf.length); + lineBuf = buf; + } + //flip the preceding backslash flag + if (c == '\\') { + precedingBackslash = !precedingBackslash; + } else { + precedingBackslash = false; + } + } else { + // reached EOL + if (isCommentLine || len == 0) { + isCommentLine = false; + isNewLine = true; + skipWhiteSpace = true; + len = 0; + lineNumber++; + continue; + } + if (inOff >= inLimit) { + inLimit = (inStream == null) + ? reader.read(inCharBuf) + : inStream.read(inByteBuf); + inOff = 0; + if (inLimit <= 0) { + if (precedingBackslash) { + len--; + } + return len; + } + } + if (precedingBackslash) { + len -= 1; + //skip the leading whitespace characters in following line + skipWhiteSpace = true; + appendedLineBegin = true; + precedingBackslash = false; + addBackslash++; + if (c == '\r') { + skipLF = true; + } + } else { + return len; + } + } + } + } + } + + private String loadConvert(char[] in, int off, int len, char[] convtBuf) { + if (convtBuf.length < len) { + int newLen = len * 2; + if (newLen < 0) { + newLen = Integer.MAX_VALUE; + } + convtBuf = new char[newLen]; + } + char aChar; + char[] out = convtBuf; + int outLen = 0; + int end = off + len; + + while (off < end) { + aChar = in[off++]; + if (aChar == '\\') { + aChar = in[off++]; + if (aChar == 'u') { + // Read the xxxx + int value = 0; + for (int i = 0; i < 4; i++) { + aChar = in[off++]; + switch (aChar) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + value = (value << 4) + aChar - '0'; + break; + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + value = (value << 4) + 10 + aChar - 'a'; + break; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + value = (value << 4) + 10 + aChar - 'A'; + break; + default: + throw new IllegalArgumentException( + "Malformed \\uxxxx encoding."); + } + } + out[outLen++] = (char) value; + } else { + if (aChar == 't') { + aChar = '\t'; + } else if (aChar == 'r') { + aChar = '\r'; + } else if (aChar == 'n') { + aChar = '\n'; + } else if (aChar == 'f') { + aChar = '\f'; + } + out[outLen++] = aChar; + } + } else { + out[outLen++] = aChar; + } + } + return new String(out, 0, outLen); + } + +} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValuePropertiesConfigSource.java b/implementation/src/main/java/io/smallrye/config/ConfigValuePropertiesConfigSource.java new file mode 100644 index 000000000..b24feb9f7 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigValuePropertiesConfigSource.java @@ -0,0 +1,34 @@ +package io.smallrye.config; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class ConfigValuePropertiesConfigSource extends MapBackedConfigValueConfigSource { + private static final long serialVersionUID = 9070158352250209380L; + + private static final String NAME_PREFIX = "ConfigValuePropertiesConfigSource[source="; + + public ConfigValuePropertiesConfigSource(URL url) throws IOException { + this(url, DEFAULT_ORDINAL); + } + + public ConfigValuePropertiesConfigSource(URL url, int defaultOrdinal) throws IOException { + this(url, NAME_PREFIX + url.toString() + "]", defaultOrdinal); + } + + private ConfigValuePropertiesConfigSource(URL url, String name, int defaultOrdinal) throws IOException { + super(name, urlToConfigValueMap(url, name, defaultOrdinal)); + } + + private static Map urlToConfigValueMap(URL locationOfProperties, String name, int ordinal) + throws IOException { + try (InputStreamReader reader = new InputStreamReader(locationOfProperties.openStream(), StandardCharsets.UTF_8)) { + ConfigValueProperties p = new ConfigValueProperties(name, ordinal); + p.load(reader); + return p; + } + } +} diff --git a/implementation/src/main/java/io/smallrye/config/MapBackedConfigValueConfigSource.java b/implementation/src/main/java/io/smallrye/config/MapBackedConfigValueConfigSource.java new file mode 100644 index 000000000..6822046e7 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/MapBackedConfigValueConfigSource.java @@ -0,0 +1,61 @@ +package io.smallrye.config; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config.common.AbstractConfigSource; + +public abstract class MapBackedConfigValueConfigSource extends AbstractConfigSource implements ConfigValueConfigSource { + private static final long serialVersionUID = -4619155951589529987L; + + private final Map properties; + + public MapBackedConfigValueConfigSource(String name, Map propertyMap) { + this(name, propertyMap, ConfigSource.DEFAULT_ORDINAL); + } + + /** + * Construct a new instance. The config source will use a default ordinal of {@code 100} and + * will use a copy of the given map if {@code copy} is {@code true}. + * + * @param name the config source name + * @param propertyMap the map to use + * @param copy {@code true} to copy the given map, {@code false} otherwise + */ + public MapBackedConfigValueConfigSource(String name, Map propertyMap, boolean copy) { + this(name, propertyMap, ConfigSource.DEFAULT_ORDINAL, copy); + } + + public MapBackedConfigValueConfigSource(String name, Map propertyMap, int defaultOrdinal) { + // TODO - read ordinal from a configuration + super(name, defaultOrdinal); + properties = Collections.unmodifiableMap(propertyMap); + } + + /** + * Construct a new instance. The config source will use the given default ordinal, and + * will use a copy of the given map if {@code copy} is {@code true}. + * + * @param name the config source name + * @param propertyMap the map to use + * @param defaultOrdinal the default ordinal to use if one is not given in the map + * @param copy {@code true} to copy the given map, {@code false} otherwise + */ + public MapBackedConfigValueConfigSource(String name, Map propertyMap, int defaultOrdinal, + boolean copy) { + this(name, copy ? new LinkedHashMap<>(propertyMap) : propertyMap, defaultOrdinal); + } + + @Override + public ConfigValue getConfigValue(final String propertyName) { + return properties.get(propertyName); + } + + @Override + public Map getConfigValueProperties() { + return properties; + } +} diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java index 40b30025e..d7f84e939 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java @@ -142,14 +142,22 @@ private ConfigSourceInterceptorContext buildInterceptorChain(final SmallRyeConfi SmallRyeConfigSourceInterceptorContext current = new SmallRyeConfigSourceInterceptorContext( (ConfigSourceInterceptor) (context, name) -> { for (ConfigSource configSource : getConfigSources()) { - String value = configSource.getValue(name); - if (value != null) { - return ConfigValue.builder() - .withName(name) - .withValue(value) - .withConfigSourceName(configSource.getName()) - .withConfigSourceOrdinal(configSource.getOrdinal()) - .build(); + if (configSource instanceof ConfigValueConfigSource) { + ConfigValueConfigSource configValueConfigSource = (ConfigValueConfigSource) configSource; + ConfigValue value = configValueConfigSource.getConfigValue(name); + if (value != null) { + return value; + } + } else { + String value = configSource.getValue(name); + if (value != null) { + return ConfigValue.builder() + .withName(name) + .withValue(value) + .withConfigSourceName(configSource.getName()) + .withConfigSourceOrdinal(configSource.getOrdinal()) + .build(); + } } } return null; diff --git a/implementation/src/test/java/io/smallrye/config/ConfigValueMapViewTest.java b/implementation/src/test/java/io/smallrye/config/ConfigValueMapViewTest.java new file mode 100644 index 000000000..f98a104a4 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigValueMapViewTest.java @@ -0,0 +1,125 @@ +package io.smallrye.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.AbstractMap; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; + +public class ConfigValueMapViewTest { + @Test + public void size() { + assertEquals(3, sampleMap().size()); + } + + @Test + public void isEmpty() { + assertTrue(new ConfigValueMapView(new HashMap<>()).isEmpty()); + } + + @Test + public void containsKey() { + final Map map = sampleMap(); + assertTrue(map.containsKey("my.prop")); + assertTrue(map.containsKey("my.null.value")); + assertTrue(map.containsKey("my.null")); + } + + @Test + public void containsValue() { + final Map map = sampleMap(); + assertTrue(map.containsValue("1234")); + } + + @Test + public void get() { + final Map map = sampleMap(); + assertEquals("1234", map.get("my.prop")); + assertNull(map.get("my.null.value")); + assertNull(map.get("my.null")); + } + + @Test(expected = UnsupportedOperationException.class) + public void put() { + sampleMap().put("x", "x"); + } + + @Test(expected = UnsupportedOperationException.class) + public void remove() { + sampleMap().remove("my.prop"); + } + + @Test(expected = UnsupportedOperationException.class) + public void putAll() { + final HashMap newMap = new HashMap<>(); + newMap.put("key", "value"); + sampleMap().putAll(newMap); + } + + @Test(expected = UnsupportedOperationException.class) + public void clear() { + sampleMap().clear(); + } + + @Test + public void keySet() { + final Set keys = sampleMap().keySet(); + assertEquals(3, keys.size()); + assertTrue(keys.contains("my.prop")); + assertTrue(keys.contains("my.null")); + assertTrue(keys.contains("my.null.value")); + assertThrows(UnsupportedOperationException.class, () -> keys.remove("my.prop")); + } + + @Test + public void entrySet() { + final Set> entries = sampleMap().entrySet(); + assertEquals(3, entries.size()); + assertTrue(entries.contains(new AbstractMap.SimpleImmutableEntry<>("my.prop", "1234"))); + assertTrue(entries.contains(new AbstractMap.SimpleImmutableEntry<>("my.null.value", null))); + assertTrue(entries.contains(new AbstractMap.SimpleImmutableEntry<>("my.null", null))); + assertThrows(UnsupportedOperationException.class, + () -> entries.remove(new AbstractMap.SimpleImmutableEntry<>("my.prop", "1234"))); + } + + @Test + public void values() { + final Collection values = sampleMap().values(); + assertEquals(3, values.size()); + assertTrue(values.contains(null)); + assertTrue(values.contains("1234")); + assertThrows(UnsupportedOperationException.class, () -> values.remove("1234")); + } + + @Test + public void configSourceMap() throws IOException { + final ConfigValuePropertiesConfigSource configSource = new ConfigValuePropertiesConfigSource( + ConfigValueMapViewTest.class.getResource("/config-values.properties")); + final Map properties = configSource.getProperties(); + + assertEquals("abc", properties.get("my.prop")); + assertEquals("abc", properties.get("my.prop")); + assertThrows(UnsupportedOperationException.class, () -> properties.remove("x")); + ; + assertThrows(UnsupportedOperationException.class, () -> properties.put("x", "x")); + assertThrows(UnsupportedOperationException.class, () -> properties.putAll(new HashMap<>())); + assertThrows(UnsupportedOperationException.class, properties::clear); + } + + private ConfigValueMapView sampleMap() { + final Map configValueMap = new HashMap<>(); + configValueMap.put("my.prop", ConfigValue.builder().withName("my.prop").withValue("1234").build()); + configValueMap.put("my.prop", ConfigValue.builder().withName("my.prop").withValue("1234").build()); + configValueMap.put("my.null.value", ConfigValue.builder().withName("my.null.value").build()); + configValueMap.put("my.null", null); + return new ConfigValueMapView(configValueMap); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesConfigSourceTest.java b/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesConfigSourceTest.java new file mode 100644 index 000000000..7ca8120a1 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesConfigSourceTest.java @@ -0,0 +1,32 @@ +package io.smallrye.config; + +import org.eclipse.microprofile.config.Config; +import org.junit.Assert; +import org.junit.Test; + +public class ConfigValuePropertiesConfigSourceTest { + @Test + public void interceptor() throws Exception { + SmallRyeConfig config = (SmallRyeConfig) buildConfig(); + + Assert.assertEquals("1", config.getValue("my.prop", String.class)); + Assert.assertEquals("20", config.getValue("my.prop.20", String.class)); + } + + private static Config buildConfig() throws Exception { + return new SmallRyeConfigBuilder() + .addDefaultSources() + .withSources(new ConfigValuePropertiesConfigSource( + ConfigValuePropertiesConfigSourceTest.class.getResource("/config-values.properties"))) + .withInterceptors((ConfigSourceInterceptor) (context, name) -> { + ConfigValue configValue = context.proceed(name); + // Return the line number instead for asssert + if (configValue != null) { + configValue = configValue.withValue(configValue.getLineNumber() + ""); + } + + return configValue; + }) + .build(); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesTest.java b/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesTest.java new file mode 100644 index 000000000..39ec95790 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesTest.java @@ -0,0 +1,80 @@ +package io.smallrye.config; + +import static org.junit.Assert.assertEquals; + +import java.io.StringReader; + +import org.junit.Test; + +public class ConfigValuePropertiesTest { + @Test + public void singleLine() throws Exception { + final ConfigValueProperties map = new ConfigValueProperties("config", 1); + map.load(new StringReader("key=value")); + + assertEquals(1, map.get("key").getLineNumber()); + } + + @Test + public void multipleLines() throws Exception { + final ConfigValueProperties map = new ConfigValueProperties("config", 1); + map.load(new StringReader( + "key=value\n" + + "key2=value\n" + + "key3=value\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "key20=value")); + + assertEquals(1, map.get("key").getLineNumber()); + assertEquals(2, map.get("key2").getLineNumber()); + assertEquals(3, map.get("key3").getLineNumber()); + assertEquals(20, map.get("key20").getLineNumber()); + } + + @Test + public void comments() throws Exception { + final ConfigValueProperties map = new ConfigValueProperties("config", 1); + map.load(new StringReader( + "key=value\n" + + "key2=value\n" + + "#comment\n" + + "#comment\n" + + "#comment\n" + + "key3=value")); + + assertEquals(1, map.get("key").getLineNumber()); + assertEquals(2, map.get("key2").getLineNumber()); + assertEquals(6, map.get("key3").getLineNumber()); + } + + @Test + public void wrapValue() throws Exception { + final ConfigValueProperties map = new ConfigValueProperties("config", 1); + map.load(new StringReader( + "key=value\\wrap\n" + + "key2=value\\\rwrap\n" + + "#comment\f\t\n" + + "#comment\r\n" + + "\\key3=value")); + + assertEquals(1, map.get("key").getLineNumber()); + assertEquals(2, map.get("key2").getLineNumber()); + assertEquals("valuewrap", map.get("key2").getValue()); + assertEquals(7, map.get("key3").getLineNumber()); + } +} diff --git a/implementation/src/test/resources/config-values.properties b/implementation/src/test/resources/config-values.properties new file mode 100644 index 000000000..f22753f97 --- /dev/null +++ b/implementation/src/test/resources/config-values.properties @@ -0,0 +1,20 @@ +my.prop=abc + + + + + + + + + + + + + + + + + + +my.prop.20=abd