diff --git a/src/main/java/ch/jalu/configme/resource/MapNormalizer.java b/src/main/java/ch/jalu/configme/resource/MapNormalizer.java index 32b8962c..471dd770 100644 --- a/src/main/java/ch/jalu/configme/resource/MapNormalizer.java +++ b/src/main/java/ch/jalu/configme/resource/MapNormalizer.java @@ -11,6 +11,21 @@ */ public class MapNormalizer { + private final boolean splitDotPaths; + + /** + * Constructor. + * + * @param splitDotPaths whether compound keys (keys with ".") should be split + */ + public MapNormalizer(boolean splitDotPaths) { + this.splitDotPaths = splitDotPaths; + } + + protected final boolean splitDotPaths() { + return splitDotPaths; + } + /** * Normalizes the raw map read from a property resource for further use in a property reader. * @@ -62,7 +77,10 @@ protected Optional> createNormalizedMapIfNeeded(Object value } protected boolean isKeyInvalid(Object key) { - return !(key instanceof String) || ((String) key).contains("."); + if (key instanceof String) { + return splitDotPaths && ((String) key).contains("."); + } + return true; } /** @@ -74,7 +92,7 @@ protected boolean isKeyInvalid(Object key) { * @param value the value to store */ protected void addValueIntoMap(Map map, String path, Object value) { - int dotPosition = path.indexOf("."); + int dotPosition = splitDotPaths ? path.indexOf(".") : -1; if (dotPosition > -1) { String pathElement = path.substring(0, dotPosition); Map mapAtPath = getOrInsertMap(map, pathElement); diff --git a/src/main/java/ch/jalu/configme/resource/YamlFileReader.java b/src/main/java/ch/jalu/configme/resource/YamlFileReader.java index 83e44f46..bb328708 100644 --- a/src/main/java/ch/jalu/configme/resource/YamlFileReader.java +++ b/src/main/java/ch/jalu/configme/resource/YamlFileReader.java @@ -36,7 +36,7 @@ public class YamlFileReader implements PropertyReader { * @param path the file to load */ public YamlFileReader(Path path) { - this(path, StandardCharsets.UTF_8); + this(path, StandardCharsets.UTF_8, true); } /** @@ -46,9 +46,20 @@ public YamlFileReader(Path path) { * @param charset the charset to read the data as */ public YamlFileReader(Path path, Charset charset) { + this(path, charset, true); + } + + /** + * Constructor. + * + * @param path the file to load + * @param charset the charset to read the data as + * @param splitDotPaths whether dots in yaml paths should be split into nested paths + */ + public YamlFileReader(Path path, Charset charset, boolean splitDotPaths) { this.path = path; this.charset = charset; - this.root = loadFile(); + this.root = loadFile(splitDotPaths); } /** @@ -165,13 +176,14 @@ private static boolean isLeafValue(Object o) { /** * Loads the values of the file. * + * @param splitDotPaths whether compound keys (keys with ".") should be split into nested paths * @return map with the values from the file */ - protected Map loadFile() { + protected Map loadFile(boolean splitDotPaths) { try (InputStream is = Files.newInputStream(path); InputStreamReader isr = new InputStreamReader(is, charset)) { Map rootMap = new Yaml().load(isr); - return normalizeMap(rootMap); + return normalizeMap(rootMap, splitDotPaths); } catch (IOException e) { throw new ConfigMeException("Could not read file '" + path + "'", e); } catch (ClassCastException e) { @@ -185,11 +197,13 @@ protected Map loadFile() { * Processes the map as read from SnakeYAML and may return a new, adjusted one. * * @param map the map to normalize + * @param splitDotPaths whether compound keys (keys with ".") should be split into nested paths * @return the normalized map (or same map if no changes are needed) */ @Nullable - protected Map normalizeMap(@Nullable Map map) { - return new MapNormalizer().normalizeMap(map); + protected Map normalizeMap(@Nullable Map map, + boolean splitDotPaths) { + return new MapNormalizer(splitDotPaths).normalizeMap(map); } // Scheduled for removal in favor of #getPath diff --git a/src/main/java/ch/jalu/configme/resource/YamlFileResource.java b/src/main/java/ch/jalu/configme/resource/YamlFileResource.java index 87223c7b..ec052e71 100644 --- a/src/main/java/ch/jalu/configme/resource/YamlFileResource.java +++ b/src/main/java/ch/jalu/configme/resource/YamlFileResource.java @@ -50,7 +50,7 @@ public YamlFileResource(File file) { @Override public PropertyReader createReader() { - return new YamlFileReader(path, options.getCharset()); + return new YamlFileReader(path, options.getCharset(), options.splitDotPaths()); } @Override diff --git a/src/main/java/ch/jalu/configme/resource/YamlFileResourceOptions.java b/src/main/java/ch/jalu/configme/resource/YamlFileResourceOptions.java index 290e2f9b..a026b2f6 100644 --- a/src/main/java/ch/jalu/configme/resource/YamlFileResourceOptions.java +++ b/src/main/java/ch/jalu/configme/resource/YamlFileResourceOptions.java @@ -12,6 +12,7 @@ public class YamlFileResourceOptions { private final Charset charset; private final ToIntFunction numberOfLinesBeforeFunction; private final int indentationSize; + private final boolean splitDotPaths; /** * Constructor. Use {@link #builder()} to instantiate option objects. @@ -19,13 +20,16 @@ public class YamlFileResourceOptions { * @param charset the charset * @param numberOfLinesBeforeFunction function defining how many lines before a path element should be in the export * @param indentationSize number of spaces to use for each level of indentation + * @param splitDotPaths whether compound keys (keys with ".") should be split into nested paths */ protected YamlFileResourceOptions(@Nullable Charset charset, @Nullable ToIntFunction numberOfLinesBeforeFunction, - int indentationSize) { + int indentationSize, + boolean splitDotPaths) { this.charset = charset == null ? StandardCharsets.UTF_8 : charset; this.numberOfLinesBeforeFunction = numberOfLinesBeforeFunction; this.indentationSize = indentationSize; + this.splitDotPaths = splitDotPaths; } public static Builder builder() { @@ -44,6 +48,10 @@ public int getIndentationSize() { return indentationSize; } + public boolean splitDotPaths() { + return splitDotPaths; + } + /** * @return the indentation to use for one level */ @@ -64,9 +72,11 @@ protected final ToIntFunction getIndentFunction() { } public static class Builder { + private Charset charset; private ToIntFunction numberOfLinesBeforeFunction; private int indentationSize = 4; + private boolean splitDotPaths = true; public Builder charset(Charset charset) { this.charset = charset; @@ -78,13 +88,18 @@ public Builder numberOfLinesBeforeFunction(ToIntFunction numberOfLi return this; } - public Builder indentationSize(final int indentationSize) { + public Builder indentationSize(int indentationSize) { this.indentationSize = indentationSize; return this; } + public Builder splitDotPaths(boolean splitDotPaths) { + this.splitDotPaths = splitDotPaths; + return this; + } + public YamlFileResourceOptions build() { - return new YamlFileResourceOptions(charset, numberOfLinesBeforeFunction, indentationSize); + return new YamlFileResourceOptions(charset, numberOfLinesBeforeFunction, indentationSize, splitDotPaths); } } } diff --git a/src/test/java/ch/jalu/configme/resource/MapNormalizerTest.java b/src/test/java/ch/jalu/configme/resource/MapNormalizerTest.java index 56425d7c..89093c75 100644 --- a/src/test/java/ch/jalu/configme/resource/MapNormalizerTest.java +++ b/src/test/java/ch/jalu/configme/resource/MapNormalizerTest.java @@ -20,11 +20,12 @@ */ class MapNormalizerTest { - private MapNormalizer mapNormalizer = new MapNormalizer(); - @Test void shouldReturnNullForNull() { - // given / when + // given + MapNormalizer mapNormalizer = new MapNormalizer(true); + + // when Map result = mapNormalizer.normalizeMap(null); // then @@ -34,6 +35,7 @@ void shouldReturnNullForNull() { @Test void shouldHandleEmptyMap() { // given + MapNormalizer mapNormalizer = new MapNormalizer(true); Map map = new HashMap<>(); // when @@ -47,6 +49,7 @@ void shouldHandleEmptyMap() { @Test void shouldKeepNormalizedMap() { // given + MapNormalizer mapNormalizer = new MapNormalizer(true); Map map1 = new HashMap<>(); Map map2 = new HashMap<>(); Map map3 = new HashMap<>(); @@ -73,6 +76,7 @@ void shouldKeepNormalizedMap() { @Test void shouldExpandPathsWithPeriod() { // given + MapNormalizer mapNormalizer = new MapNormalizer(true); Map fruits = new LinkedHashMap<>(); fruits.put("orange", "oranges"); fruits.put("apple", "apples"); @@ -100,6 +104,7 @@ void shouldExpandPathsWithPeriod() { @Test void shouldConvertKeysToStrings() { // given + MapNormalizer mapNormalizer = new MapNormalizer(true); Map map = new LinkedHashMap<>(); map.put("test", "test"); map.put("other", "other"); @@ -125,6 +130,7 @@ void shouldConvertKeysToStrings() { @Test void shouldOverrideEntriesOnClash() { // given + MapNormalizer mapNormalizer = new MapNormalizer(true); Map map = new LinkedHashMap<>(); map.put("test", "a test"); map.put("test.one", 1); @@ -143,4 +149,27 @@ void shouldOverrideEntriesOnClash() { assertThat(((Map) result.get("test")).keySet(), contains("one", "two")); assertThat(result.get("other"), equalTo(0)); } + + @Test + void shouldNotSplitDotsIfSoConfigured() { + // given + MapNormalizer mapNormalizer = new MapNormalizer(false); + Map nestedMap = new LinkedHashMap<>(); + nestedMap.put("entry.foo", "bar"); + nestedMap.put("other.entry", false); + + Map map = new LinkedHashMap<>(); + map.put("ch.jalu.sub", nestedMap); + map.put("ch.jalu.sup", 1); + map.put("ch.jalu.third", 2); + map.put(true, 3); + + // when + Map result = mapNormalizer.normalizeMap(map); + + // then + assertThat(result.keySet(), contains("ch.jalu.sub", "ch.jalu.sup", "ch.jalu.third", "true")); + Map subMap = (Map) result.get("ch.jalu.sub"); + assertThat(subMap.keySet(), contains("entry.foo", "other.entry")); + } } diff --git a/src/test/java/ch/jalu/configme/resource/YamlFileResourceNoSplitPathsTest.java b/src/test/java/ch/jalu/configme/resource/YamlFileResourceNoSplitPathsTest.java new file mode 100644 index 00000000..afa7c85d --- /dev/null +++ b/src/test/java/ch/jalu/configme/resource/YamlFileResourceNoSplitPathsTest.java @@ -0,0 +1,107 @@ +package ch.jalu.configme.resource; + +import ch.jalu.configme.TestUtils; +import ch.jalu.configme.configurationdata.ConfigurationData; +import ch.jalu.configme.configurationdata.ConfigurationDataBuilder; +import ch.jalu.configme.properties.MapProperty; +import ch.jalu.configme.properties.convertresult.PropertyValue; +import ch.jalu.configme.properties.types.EnumPropertyType; +import ch.jalu.configme.properties.types.PrimitivePropertyType; +import ch.jalu.configme.samples.TestEnum; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import static ch.jalu.configme.properties.PropertyInitializer.mapProperty; +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; + +/** + * Verifies that YAML paths with '.' are not split into nested paths when not configured. + *

+ * @see Issue #214 + */ +class YamlFileResourceNoSplitPathsTest { + + @TempDir + public Path tempFolder; + + @Test + void shouldLoadFilesWithDotsSuccessfully() throws IOException { + // given + Path file = TestUtils.createTemporaryFile(tempFolder); + String yaml = "groups:" + + "\n com.example.basic: 2" + + "\n com.example.advanced: 5" + + "\n com.example.premium: 10" + + "\n com.example.vip: 20"; + Files.write(file, yaml.getBytes(StandardCharsets.UTF_8)); + + MapProperty groupsProperty = mapProperty(PrimitivePropertyType.INTEGER) + .path("groups") + .build(); + YamlFileResourceOptions fileResourceOptions = YamlFileResourceOptions.builder() + .splitDotPaths(false) + .build(); + YamlFileResource resource = new YamlFileResource(file, fileResourceOptions); + + // when + PropertyValue> readGroups = groupsProperty.determineValue(resource.createReader()); + + // then + assertThat(readGroups.isValidInResource(), equalTo(true)); + Map expectedValue = new HashMap<>(); + expectedValue.put("com.example.basic", 2); + expectedValue.put("com.example.advanced", 5); + expectedValue.put("com.example.premium", 10); + expectedValue.put("com.example.vip", 20); + assertThat(readGroups.getValue(), equalTo(expectedValue)); + } + + @Test + @Disabled + void shouldExportMapWithDotsWithoutSplittingPaths() throws IOException { + // given + Path file = TestUtils.createTemporaryFile(tempFolder); + MapProperty sectionsProperty = mapProperty(EnumPropertyType.of(TestEnum.class)) + .path("sections") + .build(); + ConfigurationData configurationData = ConfigurationDataBuilder.createConfiguration(singletonList(sectionsProperty)); + + Map newSections = new LinkedHashMap<>(); + newSections.put("org.example.one", TestEnum.FOURTH); + newSections.put("org.example.second.a", TestEnum.FIRST); + newSections.put("org.example.second.b", TestEnum.SECOND); + newSections.put("org.example.third", TestEnum.THIRD); + newSections.put("abc", TestEnum.FOURTH); + configurationData.setValue(sectionsProperty, newSections); + + YamlFileResourceOptions fileResourceOptions = YamlFileResourceOptions.builder() + .splitDotPaths(false) + .build(); + YamlFileResource resource = new YamlFileResource(file, fileResourceOptions); + + // when + resource.exportProperties(configurationData); + + // then + assertThat(Files.readAllLines(file), contains( + "sections:", + " org.example.one: 'FOURTH'", + " org.example.second.a: 'FIRST'", + " org.example.second.b: 'SECOND'", + " org.example.third: 'THIRD'", + " abc: 'FOURTH'" + )); + } +} diff --git a/src/test/java/ch/jalu/configme/resource/YamlFileResourceOptionsTest.java b/src/test/java/ch/jalu/configme/resource/YamlFileResourceOptionsTest.java index d6719f2e..4a1a1897 100644 --- a/src/test/java/ch/jalu/configme/resource/YamlFileResourceOptionsTest.java +++ b/src/test/java/ch/jalu/configme/resource/YamlFileResourceOptionsTest.java @@ -26,6 +26,7 @@ void shouldKeepConfiguredValues() { .numberOfLinesBeforeFunction(lineFunction) .charset(StandardCharsets.UTF_16BE) .indentationSize(2) + .splitDotPaths(false) .build(); // then @@ -35,6 +36,7 @@ void shouldKeepConfiguredValues() { assertThat(options.getNumberOfEmptyLinesBefore(pathElement), equalTo(3)); assertThat(options.getIndentationSize(), equalTo(2)); assertThat(options.getIndentation(), equalTo(" ")); + assertThat(options.splitDotPaths(), equalTo(false)); } @Test @@ -49,5 +51,6 @@ void shouldCreateOptionsWithDefaults() { assertThat(options.getIndentation(), equalTo(" ")); PathElement pathElement = new PathElement(3, "test", emptyList(), false); assertThat(options.getNumberOfEmptyLinesBefore(pathElement), equalTo(0)); + assertThat(options.splitDotPaths(), equalTo(true)); } }