Skip to content

Commit

Permalink
#214 Introduce YAML resource option to preserve dots in paths
Browse files Browse the repository at this point in the history
- Add option to preserve dots in paths for Map properties
- Open point: writing to resource still splits paths
  • Loading branch information
ljacqu committed Sep 12, 2021
1 parent 3703dac commit 9d47345
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 15 deletions.
22 changes: 20 additions & 2 deletions src/main/java/ch/jalu/configme/resource/MapNormalizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -62,7 +77,10 @@ protected Optional<Map<String, Object>> 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;
}

/**
Expand All @@ -74,7 +92,7 @@ protected boolean isKeyInvalid(Object key) {
* @param value the value to store
*/
protected void addValueIntoMap(Map<String, Object> 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<String, Object> mapAtPath = getOrInsertMap(map, pathElement);
Expand Down
26 changes: 20 additions & 6 deletions src/main/java/ch/jalu/configme/resource/YamlFileReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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<String, Object> loadFile() {
protected Map<String, Object> loadFile(boolean splitDotPaths) {
try (InputStream is = Files.newInputStream(path);
InputStreamReader isr = new InputStreamReader(is, charset)) {
Map<Object, Object> 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) {
Expand All @@ -185,11 +197,13 @@ protected Map<String, Object> 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<String, Object> normalizeMap(@Nullable Map<Object, Object> map) {
return new MapNormalizer().normalizeMap(map);
protected Map<String, Object> normalizeMap(@Nullable Map<Object, Object> map,
boolean splitDotPaths) {
return new MapNormalizer(splitDotPaths).normalizeMap(map);
}

// Scheduled for removal in favor of #getPath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,24 @@ public class YamlFileResourceOptions {
private final Charset charset;
private final ToIntFunction<PathElement> numberOfLinesBeforeFunction;
private final int indentationSize;
private final boolean splitDotPaths;

/**
* Constructor. Use {@link #builder()} to instantiate option objects.
*
* @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<PathElement> 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() {
Expand All @@ -44,6 +48,10 @@ public int getIndentationSize() {
return indentationSize;
}

public boolean splitDotPaths() {
return splitDotPaths;
}

/**
* @return the indentation to use for one level
*/
Expand All @@ -64,9 +72,11 @@ protected final ToIntFunction<PathElement> getIndentFunction() {
}

public static class Builder {

private Charset charset;
private ToIntFunction<PathElement> numberOfLinesBeforeFunction;
private int indentationSize = 4;
private boolean splitDotPaths = true;

public Builder charset(Charset charset) {
this.charset = charset;
Expand All @@ -78,13 +88,18 @@ public Builder numberOfLinesBeforeFunction(ToIntFunction<PathElement> 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);
}
}
}
35 changes: 32 additions & 3 deletions src/test/java/ch/jalu/configme/resource/MapNormalizerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
*/
class MapNormalizerTest {

private MapNormalizer mapNormalizer = new MapNormalizer();

@Test
void shouldReturnNullForNull() {
// given / when
// given
MapNormalizer mapNormalizer = new MapNormalizer(true);

// when
Map<String, Object> result = mapNormalizer.normalizeMap(null);

// then
Expand All @@ -34,6 +35,7 @@ void shouldReturnNullForNull() {
@Test
void shouldHandleEmptyMap() {
// given
MapNormalizer mapNormalizer = new MapNormalizer(true);
Map<Object, Object> map = new HashMap<>();

// when
Expand All @@ -47,6 +49,7 @@ void shouldHandleEmptyMap() {
@Test
void shouldKeepNormalizedMap() {
// given
MapNormalizer mapNormalizer = new MapNormalizer(true);
Map<Object, Object> map1 = new HashMap<>();
Map<Object, Object> map2 = new HashMap<>();
Map<Object, Object> map3 = new HashMap<>();
Expand All @@ -73,6 +76,7 @@ void shouldKeepNormalizedMap() {
@Test
void shouldExpandPathsWithPeriod() {
// given
MapNormalizer mapNormalizer = new MapNormalizer(true);
Map<Object, Object> fruits = new LinkedHashMap<>();
fruits.put("orange", "oranges");
fruits.put("apple", "apples");
Expand Down Expand Up @@ -100,6 +104,7 @@ void shouldExpandPathsWithPeriod() {
@Test
void shouldConvertKeysToStrings() {
// given
MapNormalizer mapNormalizer = new MapNormalizer(true);
Map<Object, Object> map = new LinkedHashMap<>();
map.put("test", "test");
map.put("other", "other");
Expand All @@ -125,6 +130,7 @@ void shouldConvertKeysToStrings() {
@Test
void shouldOverrideEntriesOnClash() {
// given
MapNormalizer mapNormalizer = new MapNormalizer(true);
Map<Object, Object> map = new LinkedHashMap<>();
map.put("test", "a test");
map.put("test.one", 1);
Expand All @@ -143,4 +149,27 @@ void shouldOverrideEntriesOnClash() {
assertThat(((Map<String, Object>) result.get("test")).keySet(), contains("one", "two"));
assertThat(result.get("other"), equalTo(0));
}

@Test
void shouldNotSplitDotsIfSoConfigured() {
// given
MapNormalizer mapNormalizer = new MapNormalizer(false);
Map<Object, Object> nestedMap = new LinkedHashMap<>();
nestedMap.put("entry.foo", "bar");
nestedMap.put("other.entry", false);

Map<Object, Object> 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<String, Object> result = mapNormalizer.normalizeMap(map);

// then
assertThat(result.keySet(), contains("ch.jalu.sub", "ch.jalu.sup", "ch.jalu.third", "true"));
Map<String, Object> subMap = (Map) result.get("ch.jalu.sub");
assertThat(subMap.keySet(), contains("entry.foo", "other.entry"));
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* @see <a href="https://github.com/AuthMe/ConfigMe/issues/214">Issue #214</a>
*/
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<Integer> groupsProperty = mapProperty(PrimitivePropertyType.INTEGER)
.path("groups")
.build();
YamlFileResourceOptions fileResourceOptions = YamlFileResourceOptions.builder()
.splitDotPaths(false)
.build();
YamlFileResource resource = new YamlFileResource(file, fileResourceOptions);

// when
PropertyValue<Map<String, Integer>> readGroups = groupsProperty.determineValue(resource.createReader());

// then
assertThat(readGroups.isValidInResource(), equalTo(true));
Map<String, Integer> 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<TestEnum> sectionsProperty = mapProperty(EnumPropertyType.of(TestEnum.class))
.path("sections")
.build();
ConfigurationData configurationData = ConfigurationDataBuilder.createConfiguration(singletonList(sectionsProperty));

Map<String, TestEnum> 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'"
));
}
}
Loading

0 comments on commit 9d47345

Please sign in to comment.