Skip to content

Commit aa5d3f0

Browse files
Handle numeric conversions for non-string raw values.
For implementations of AbstractConfig that return non-string values from getRawProperty(), be smarter about doing numeric conversions. Without this, users are forced to guess if the property source will interpret a given number as an integer or a long and code to that, instead of to the types that make sense for their own use case. Fixes: #653
1 parent 424287b commit aa5d3f0

File tree

3 files changed

+141
-15
lines changed

3 files changed

+141
-15
lines changed

archaius2-core/src/main/java/com/netflix/archaius/config/AbstractConfig.java

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -254,27 +254,74 @@ protected <T> T getValue(Type type, String key) {
254254
}
255255
}
256256

257+
@SuppressWarnings("unchecked")
257258
protected <T> T getValueWithDefault(Type type, String key, T defaultValue) {
258259
Object rawProp = getRawProperty(key);
260+
261+
// Not found. Return the default.
259262
if (rawProp == null) {
260263
return defaultValue;
261264
}
265+
266+
// raw prop is a String. Decode it or fail.
262267
if (rawProp instanceof String) {
263268
try {
264269
String value = resolve(rawProp.toString());
265270
return decoder.decode(type, value);
266-
} catch (NumberFormatException e) {
271+
} catch (RuntimeException e) {
267272
return parseError(key, rawProp.toString(), e);
268273
}
269-
} else if (type instanceof Class) {
274+
}
275+
276+
if (type instanceof Class) {
277+
// Caller wants a simple class.
270278
Class<?> cls = (Class<?>) type;
271-
if (cls.isInstance(rawProp) || cls.isPrimitive()) {
279+
280+
// The raw object is already of the right type
281+
if (cls.isInstance(rawProp)) {
282+
return (T) rawProp;
283+
}
284+
285+
// Caller wants a string
286+
if (String.class.isAssignableFrom(cls)) {
287+
return (T) rawProp.toString();
288+
}
289+
290+
// Caller wants an unwrapped boolean.
291+
if (rawProp instanceof Boolean && cls == boolean.class) {
272292
return (T) rawProp;
273293
}
294+
295+
// Caller wants a number AND we have one. Handle widening and narrowing conversions.
296+
// Char is not included here. It's not a Number and the semantics of converting it to/from a number or a
297+
// string have rough edges. Just ask users to avoid it.
298+
if (rawProp instanceof Number
299+
&& ( Number.class.isAssignableFrom(cls)
300+
|| ( cls.isPrimitive() && cls != char.class ))) { // We handled boolean above, so if cls is a primitive and not char then it's a number type
301+
if (cls == int.class || cls == Integer.class) {
302+
return (T) Integer.valueOf(((Number) rawProp).intValue());
303+
}
304+
if (cls == long.class || cls == Long.class) {
305+
return (T) Long.valueOf(((Number) rawProp).longValue());
306+
}
307+
if (cls == double.class || cls == Double.class) {
308+
return (T) Double.valueOf(((Number) rawProp).doubleValue());
309+
}
310+
if (cls == float.class || cls == Float.class) {
311+
return (T) Float.valueOf(((Number) rawProp).floatValue());
312+
}
313+
if (cls == short.class || cls == Short.class) {
314+
return (T) Short.valueOf(((Number) rawProp).shortValue());
315+
}
316+
if (cls == byte.class || cls == Byte.class) {
317+
return (T) Byte.valueOf(((Number) rawProp).byteValue());
318+
}
319+
}
274320
}
275321

322+
// Nothing matches (ie, caller wants a ParametrizedType, or the rawProp is not easily cast to the desired type)
276323
return parseError(key, rawProp.toString(),
277-
new NumberFormatException("Property " + rawProp.toString() + " is of wrong format " + type.getTypeName()));
324+
new IllegalArgumentException("Property " + rawProp + " is not convertible to " + type.getTypeName()));
278325
}
279326

280327
@Override
@@ -284,7 +331,7 @@ public String resolve(String value) {
284331

285332
@Override
286333
public <T> T resolve(String value, Class<T> type) {
287-
return getDecoder().decode(type, resolve(value));
334+
return getDecoder().decode((Type) type, resolve(value));
288335
}
289336

290337
@Override
@@ -384,14 +431,15 @@ public <T> List<T> getList(String key, Class<T> type) {
384431
return notFound(key);
385432
}
386433
String[] parts = value.split(getListDelimiter());
387-
List<T> result = new ArrayList<T>();
434+
List<T> result = new ArrayList<>();
388435
for (String part : parts) {
389-
result.add(decoder.decode(type, part));
436+
result.add(decoder.decode((Type) type, part));
390437
}
391438
return result;
392439
}
393440

394441
@Override
442+
@SuppressWarnings("rawtypes") // Required by legacy API
395443
public List getList(String key) {
396444
String value = getString(key);
397445
if (value == null) {
@@ -402,6 +450,7 @@ public List getList(String key) {
402450
}
403451

404452
@Override
453+
@SuppressWarnings("rawtypes") // Required by legacy API
405454
public List getList(String key, List defaultValue) {
406455
String value = getString(key, null);
407456
if (value == null) {

archaius2-core/src/test/java/com/netflix/archaius/config/AbstractConfigTest.java

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
package com.netflix.archaius.config;
1717

1818
import java.util.Collections;
19+
import java.util.HashMap;
1920
import java.util.Iterator;
21+
import java.util.Map;
2022
import java.util.function.BiConsumer;
2123

2224
import org.junit.jupiter.api.Test;
@@ -27,9 +29,20 @@
2729
public class AbstractConfigTest {
2830

2931
private final AbstractConfig config = new AbstractConfig() {
32+
private final Map<String, Object> entries = new HashMap<>();
33+
34+
{
35+
entries.put("foo", "bar");
36+
entries.put("byte", (byte) 42);
37+
entries.put("int", 42);
38+
entries.put("long", 42L);
39+
entries.put("float", 42.0f);
40+
entries.put("double", 42.0d);
41+
}
42+
3043
@Override
3144
public boolean containsKey(String key) {
32-
return "foo".equals(key);
45+
return entries.containsKey(key);
3346
}
3447

3548
@Override
@@ -39,35 +52,90 @@ public boolean isEmpty() {
3952

4053
@Override
4154
public Iterator<String> getKeys() {
42-
return Collections.singletonList("foo").iterator();
55+
return Collections.unmodifiableSet(entries.keySet()).iterator();
4356
}
4457

4558
@Override
4659
public Object getRawProperty(String key) {
47-
if ("foo".equals(key)) {
48-
return "bar";
49-
}
50-
return null;
60+
return entries.get(key);
5161
}
5262

5363
@Override
5464
public void forEachProperty(BiConsumer<String, Object> consumer) {
55-
consumer.accept("foo", "bar");
65+
entries.forEach(consumer);
5666
}
5767
};
5868

5969
@Test
60-
public void testGet() throws Exception {
70+
public void testGet() {
6171
assertEquals("bar", config.get(String.class, "foo"));
6272
}
6373

6474
@Test
6575
public void getExistingProperty() {
76+
//noinspection OptionalGetWithoutIsPresent
6677
assertEquals("bar", config.getProperty("foo").get());
6778
}
6879

6980
@Test
7081
public void getNonExistentProperty() {
7182
assertFalse(config.getProperty("non_existent").isPresent());
7283
}
84+
85+
@Test
86+
public void testGetRawNumerics() {
87+
// First, get each entry as its expected type and the corresponding wrapper.
88+
assertEquals(42, config.get(int.class, "int"));
89+
assertEquals(42, config.get(Integer.class, "int"));
90+
assertEquals(42L, config.get(long.class, "long"));
91+
assertEquals(42L, config.get(Long.class, "long"));
92+
assertEquals((byte) 42, config.get(byte.class, "byte"));
93+
assertEquals((byte) 42, config.get(Byte.class, "byte"));
94+
assertEquals(42.0f, config.get(float.class, "float"));
95+
assertEquals(42.0f, config.get(Float.class, "float"));
96+
assertEquals(42.0d, config.get(double.class, "double"));
97+
assertEquals(42.0d, config.get(Double.class, "double"));
98+
99+
// Then, get each entry as a string
100+
assertEquals("42", config.get(String.class, "int"));
101+
assertEquals("42", config.get(String.class, "long"));
102+
assertEquals("42", config.get(String.class, "byte"));
103+
assertEquals("42.0", config.get(String.class, "float"));
104+
assertEquals("42.0", config.get(String.class, "double"));
105+
106+
// Then, narrowed types
107+
assertEquals((byte) 42, config.get(byte.class, "int"));
108+
assertEquals((byte) 42, config.get(byte.class, "long"));
109+
assertEquals((byte) 42, config.get(byte.class, "float"));
110+
assertEquals((byte) 42, config.get(byte.class, "double"));
111+
assertEquals(42.0f, config.get(double.class, "double"));
112+
113+
// Then, widened
114+
assertEquals(42L, config.get(long.class, "int"));
115+
assertEquals(42L, config.get(long.class, "byte"));
116+
assertEquals(42L, config.get(long.class, "float"));
117+
assertEquals(42L, config.get(long.class, "double"));
118+
assertEquals(42.0d, config.get(double.class, "float"));
119+
120+
// On floating point
121+
assertEquals(42.0f, config.get(float.class, "int"));
122+
assertEquals(42.0f, config.get(float.class, "byte"));
123+
assertEquals(42.0f, config.get(float.class, "long"));
124+
assertEquals(42.0f, config.get(float.class, "double"));
125+
126+
// As doubles
127+
assertEquals(42.0d, config.get(double.class, "int"));
128+
assertEquals(42.0d, config.get(double.class, "byte"));
129+
assertEquals(42.0d, config.get(double.class, "long"));
130+
assertEquals(42.0d, config.get(double.class, "float"));
131+
132+
// Narrowed types in wrapper classes
133+
assertEquals((byte) 42, config.get(Byte.class, "int"));
134+
assertEquals((byte) 42, config.get(Byte.class, "long"));
135+
assertEquals((byte) 42, config.get(Byte.class, "float"));
136+
137+
// Widened types in wrappers
138+
assertEquals(42L, config.get(Long.class, "int"));
139+
assertEquals(42L, config.get(Long.class, "byte"));
140+
}
73141
}

archaius2-core/src/test/java/com/netflix/archaius/config/MapConfigTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,15 @@ public void numericInterpolationShouldWork() {
183183
assertEquals(123L, (long) config.getLong("value"));
184184
}
185185

186+
@Test
187+
public void numericInterpolationShouldWork_withNonStringValues() {
188+
Config config = MapConfig.builder()
189+
.put("default", 123)
190+
.put("value", "${default}")
191+
.build();
192+
assertEquals(123L, (long) config.getLong("value"));
193+
}
194+
186195
@Test
187196
public void getKeys() {
188197
Map<String, String> props = new HashMap<>();

0 commit comments

Comments
 (0)