Skip to content

Commit 2ccc783

Browse files
authored
Merge pull request #504 from AuthMe/449-map-property-type
#449 Create Map PropertyType
2 parents e9b52ef + 3ca564b commit 2ccc783

File tree

4 files changed

+328
-52
lines changed

4 files changed

+328
-52
lines changed
Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
package ch.jalu.configme.properties;
22

3-
import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder;
3+
import ch.jalu.configme.properties.types.MapPropertyType;
44
import ch.jalu.configme.properties.types.PropertyType;
5-
import ch.jalu.configme.resource.PropertyReader;
65
import org.jetbrains.annotations.NotNull;
7-
import org.jetbrains.annotations.Nullable;
86

97
import java.util.Collections;
10-
import java.util.LinkedHashMap;
118
import java.util.Map;
12-
import java.util.Objects;
139

1410
/**
15-
* Property for an immutable map whose keys is of type String and whose values can be configured.
16-
* The map retains the order of the elements.
11+
* Property for a map with String keys and a configurable value type. The map retains the order of the elements.
12+
* Maps produced by this property are guaranteed to never have a null key or null value.
1713
*
1814
* @param <V> the value type of the map
1915
*/
20-
public class MapProperty<V> extends BaseProperty<Map<String, V>> {
21-
22-
private final PropertyType<V> valueType;
16+
public class MapProperty<V> extends TypeBasedProperty<Map<String, V>> {
2317

2418
/**
2519
* Constructor. Builds a {@link MapProperty} with an empty map as default value.
@@ -28,9 +22,7 @@ public class MapProperty<V> extends BaseProperty<Map<String, V>> {
2822
* @param valueType the property type of the values
2923
*/
3024
public MapProperty(@NotNull String path, @NotNull PropertyType<V> valueType) {
31-
super(path, Collections.emptyMap());
32-
Objects.requireNonNull(valueType, "valueType");
33-
this.valueType = valueType;
25+
super(path, new MapPropertyType<>(valueType), Collections.emptyMap());
3426
}
3527

3628
/**
@@ -41,48 +33,46 @@ public MapProperty(@NotNull String path, @NotNull PropertyType<V> valueType) {
4133
* @param defaultValue the default value of the property
4234
*/
4335
public MapProperty(@NotNull String path, @NotNull PropertyType<V> valueType, @NotNull Map<String, V> defaultValue) {
44-
super(path, Collections.unmodifiableMap(defaultValue));
45-
Objects.requireNonNull(valueType, "valueType");
46-
this.valueType = valueType;
36+
super(path, new MapPropertyType<>(valueType), defaultValue);
4737
}
4838

49-
@Override
50-
protected @Nullable Map<String, V> getFromReader(@NotNull PropertyReader reader,
51-
@NotNull ConvertErrorRecorder errorRecorder) {
52-
Object rawObject = reader.getObject(getPath());
53-
54-
if (!(rawObject instanceof Map<?, ?>)) {
55-
return null;
56-
}
57-
58-
Map<?, ?> rawMap = (Map<?, ?>) rawObject;
59-
Map<String, V> map = new LinkedHashMap<>();
60-
61-
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
62-
String path = entry.getKey().toString();
63-
V value = valueType.convert(entry.getValue(), errorRecorder);
64-
65-
if (value != null) {
66-
map.put(path, value);
67-
}
68-
}
69-
70-
return postProcessMap(map);
39+
/**
40+
* Constructor. Use {@link #withMapType}.
41+
*
42+
* @param mapType the map type
43+
* @param path the path of the property
44+
* @param defaultValue the default value of the property
45+
*/
46+
// Constructor arguments are usually (path, type, defaultValue), but this is not possible here because there
47+
// are other constructors with the same argument order.
48+
protected MapProperty(@NotNull PropertyType<Map<String, V>> mapType, @NotNull String path,
49+
@NotNull Map<String, V> defaultValue) {
50+
super(path, mapType, defaultValue);
7151
}
7252

73-
@Override
74-
public @NotNull Object toExportValue(@NotNull Map<String, V> value) {
75-
Map<String, Object> exportMap = new LinkedHashMap<>();
76-
77-
for (Map.Entry<String, V> entry : value.entrySet()) {
78-
exportMap.put(entry.getKey(), valueType.toExportValue(entry.getValue()));
79-
}
80-
81-
return exportMap;
53+
/**
54+
* Creates a new map property with the given path and type. An empty map is set as default value.
55+
*
56+
* @param path the path of the property
57+
* @param mapType the map type
58+
* @param <V> the type of the values in the map
59+
* @return a new map property
60+
*/
61+
public static <V> MapProperty<V> withMapType(@NotNull String path, @NotNull PropertyType<Map<String, V>> mapType) {
62+
return new MapProperty<>(mapType, path, Collections.emptyMap());
8263
}
8364

84-
/* Allows to modify the map once its fully built based on the values in the property reader. */
85-
protected @NotNull Map<String, V> postProcessMap(@NotNull Map<String, V> constructedMap) {
86-
return Collections.unmodifiableMap(constructedMap);
65+
/**
66+
* Creates a new map property with the given path, type and default value.
67+
*
68+
* @param path the path of the property
69+
* @param mapType the map type
70+
* @param defaultValue the default value of the property
71+
* @param <V> the type of the values in the map
72+
* @return a new map property
73+
*/
74+
public static <V> MapProperty<V> withMapType(@NotNull String path, @NotNull PropertyType<Map<String, V>> mapType,
75+
@NotNull Map<String, V> defaultValue) {
76+
return new MapProperty<>(mapType, path, defaultValue);
8777
}
8878
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package ch.jalu.configme.properties.types;
2+
3+
import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.jetbrains.annotations.Nullable;
6+
7+
import java.util.LinkedHashMap;
8+
import java.util.Map;
9+
10+
/**
11+
* Property types for maps with strings as keys and any value type. The produced maps keep insertion order (as given
12+
* in the property resource). Maps produced by this type never have a null key or a null value.
13+
*
14+
* @param <V> the type of values in the map
15+
*/
16+
public class MapPropertyType<V> implements PropertyType<Map<String, V>> {
17+
18+
private final PropertyType<V> valueType;
19+
20+
/**
21+
* Constructor.
22+
*
23+
* @param valueType property type to handle the map's values
24+
*/
25+
public MapPropertyType(@NotNull PropertyType<V> valueType) {
26+
this.valueType = valueType;
27+
}
28+
29+
@Override
30+
public @Nullable Map<String, V> convert(@Nullable Object object, @NotNull ConvertErrorRecorder errorRecorder) {
31+
if (!(object instanceof Map<?, ?>)) {
32+
return null;
33+
}
34+
35+
Map<?, ?> rawMap = (Map<?, ?>) object;
36+
Map<String, V> map = createResultMap();
37+
38+
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
39+
String key = convertKeyToString(entry.getKey());
40+
V value = valueType.convert(entry.getValue(), errorRecorder);
41+
42+
if (key != null && value != null) {
43+
V previous = map.put(key, value);
44+
if (previous != null) {
45+
errorRecorder.setHasError("Duplicate key detected: '" + key + "'");
46+
}
47+
} else {
48+
errorRecorder.setHasError("Key or value could not be converted for key '" + entry.getKey() + "'");
49+
}
50+
}
51+
return map;
52+
}
53+
54+
@Override
55+
public @NotNull Map<String, Object> toExportValue(@NotNull Map<String, V> value) {
56+
Map<String, Object> exportMap = new LinkedHashMap<>(value.size());
57+
for (Map.Entry<String, V> entry : value.entrySet()) {
58+
exportMap.put(entry.getKey(), valueType.toExportValue(entry.getValue()));
59+
}
60+
return exportMap;
61+
}
62+
63+
public final @NotNull PropertyType<V> getValueType() {
64+
return valueType;
65+
}
66+
67+
/**
68+
* @return new map to which entries are added when converting
69+
*/
70+
protected @NotNull Map<String, V> createResultMap() {
71+
return new LinkedHashMap<>();
72+
}
73+
74+
/**
75+
* Converts the given key value from the property reader to a String to be used as key. Returns null if
76+
* the value is invalid and has no appropriate representation.
77+
*
78+
* @param key the key to convert
79+
* @return string key, or null if not applicable
80+
*/
81+
protected @Nullable String convertKeyToString(@Nullable Object key) {
82+
return key == null ? null : key.toString();
83+
}
84+
}

src/test/java/ch/jalu/configme/properties/MapPropertyTest.java

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ch.jalu.configme.TestUtils;
44
import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder;
55
import ch.jalu.configme.properties.convertresult.PropertyValue;
6+
import ch.jalu.configme.properties.types.MapPropertyType;
67
import ch.jalu.configme.properties.types.NumberType;
78
import ch.jalu.configme.properties.types.PropertyType;
89
import ch.jalu.configme.properties.types.StringType;
@@ -21,6 +22,7 @@
2122
import java.util.HashMap;
2223
import java.util.LinkedHashMap;
2324
import java.util.Map;
25+
import java.util.TreeMap;
2426

2527
import static ch.jalu.configme.TestUtils.isErrorValueOf;
2628
import static ch.jalu.configme.TestUtils.isValidValueOf;
@@ -41,7 +43,7 @@ class MapPropertyTest {
4143
private PropertyReader reader;
4244

4345
@TempDir
44-
public Path temporaryFolder;
46+
Path temporaryFolder;
4547

4648
@Test
4749
void shouldReturnValueFromResource() {
@@ -118,7 +120,7 @@ void shouldUseEmptyMapAsDefaultValue() {
118120
// given
119121
MapProperty<Integer> property = new MapProperty<>("test", NumberType.INTEGER);
120122

121-
//when
123+
// when
122124
Map<String, Integer> actualDefaultValue = property.getDefaultValue();
123125
String actualPath = property.getPath();
124126

@@ -127,6 +129,63 @@ void shouldUseEmptyMapAsDefaultValue() {
127129
assertThat(actualPath, equalTo("test"));
128130
}
129131

132+
@Test
133+
void shouldBuildMapWithCustomPropertyType() {
134+
// given
135+
PropertyType<Map<String, Double>> customMapType = new MapPropertyType<Double>(NumberType.DOUBLE) {
136+
@Override
137+
protected @Nullable String convertKeyToString(@Nullable Object key) {
138+
if (key instanceof Integer) {
139+
return "k" + key;
140+
}
141+
return null;
142+
}
143+
};
144+
MapProperty<Double> mapProperty = MapProperty.withMapType("mapping", customMapType);
145+
146+
Map<Integer, Integer> inputMap = new LinkedHashMap<>();
147+
inputMap.put(1, 1);
148+
inputMap.put(4, 4);
149+
given(reader.getObject("mapping")).willReturn(inputMap);
150+
151+
// when
152+
PropertyValue<Map<String, Double>> propertyValue = mapProperty.determineValue(reader);
153+
154+
// then
155+
assertThat(propertyValue.isValidInResource(), equalTo(true));
156+
assertThat(propertyValue.getValue().keySet(), contains("k1", "k4"));
157+
assertThat(propertyValue.getValue().values(), contains(1.0, 4.0));
158+
assertThat(mapProperty.getDefaultValue(), anEmptyMap());
159+
}
160+
161+
@Test
162+
void shouldBuildMapWithCustomPropertyTypeAndDefaultValue() {
163+
// given
164+
PropertyType<Map<String, Double>> customMapType = new MapPropertyType<Double>(NumberType.DOUBLE) {
165+
@Override
166+
protected @NotNull Map<String, Double> createResultMap() {
167+
return new TreeMap<>();
168+
}
169+
};
170+
Map<String, Double> defaultMap = new TreeMap<>();
171+
defaultMap.put("7", 3.5);
172+
MapProperty<Double> mapProperty = MapProperty.withMapType("mapping", customMapType, defaultMap);
173+
174+
Map<Integer, Integer> inputMap = new LinkedHashMap<>();
175+
inputMap.put(8, 3);
176+
inputMap.put(4, 6);
177+
given(reader.getObject("mapping")).willReturn(inputMap);
178+
179+
// when
180+
PropertyValue<Map<String, Double>> propertyValue = mapProperty.determineValue(reader);
181+
182+
// then
183+
assertThat(propertyValue.isValidInResource(), equalTo(true));
184+
assertThat(propertyValue.getValue().keySet(), contains("4", "8"));
185+
assertThat(propertyValue.getValue().values(), contains(6.0, 3.0));
186+
assertThat(mapProperty.getDefaultValue(), equalTo(defaultMap));
187+
}
188+
130189
private static Map<String, String> createSampleMap() {
131190
Map<String, String> map = new HashMap<>();
132191
map.put("test", "keks");

0 commit comments

Comments
 (0)