add universal type converter for config

This commit is contained in:
Jonathan Shook 2021-07-22 17:38:14 -05:00
parent b76afa38bb
commit b227f0bccc
4 changed files with 387 additions and 0 deletions

View File

@ -0,0 +1,163 @@
package io.nosqlbench.nb.api.config.standard;
import org.apache.commons.lang3.ClassUtils;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* Shenanigans in the java type system, particularly those around boxing,
* generics, type-erasure and primitive conversions have brought us here
* in our attempt to simplify things.
*
* In the future, when Java has fewer special cases in the type system,
* this class can be removed.
*
* General purpose strategies for conversion
* can be injected into the {@link #do_convert(Object, Class)} and
* {@link #canConvert(Object, Class)}
* methods.
*/
public class NBTypeConverter {
private final static List<Class<? extends NBTypeConverters>> CONVERTERS = List.of(NBTypeSafeConversions.class);
/**
* The core types should have full set closure on conversions are not narrowing
*/
public static Set<Class<?>> CORE_TYPES = new HashSet<>() {{
add(String.class);
addAll(List.of(byte.class, Byte.class, char.class, Character.class, short.class, Short.class));
addAll(List.of(int.class, Integer.class, long.class, Long.class));
addAll(List.of(float.class, Float.class, double.class, Double.class));
}};
public static <I, O> boolean canConvert(I input, Class<O> outc) {
if (outc.equals(input.getClass())) return true; // no conversion needed
if (outc.isAssignableFrom(input.getClass())) return true; // assignable
if (ClassUtils.isAssignable(input.getClass(), outc, true)) return true; // assignable with boxing
if (String.class.isAssignableFrom(outc)) return true; // all things can be strings
if (outc.isPrimitive() && outc != boolean.class && outc != void.class && (input instanceof Number)) return true; // via Number conversions
return (lookup(input.getClass(), outc) != null); // fall-through to helper method lookup
}
private static <I, O> Method lookup(Class<I> input, Class<O> output) {
Method candidate = null;
for (Class<? extends NBTypeConverters> converters : CONVERTERS) {
try {
candidate = converters.getMethod("to_" + output.getSimpleName(), input);
break;
} catch (NoSuchMethodException ignored) {
}
}
if (candidate == null) {
return null;
}
if (!candidate.getReturnType().equals(output)) {
return null;
}
if (!((candidate.getModifiers() & Modifier.STATIC) > 0)) {
return null;
}
return candidate;
}
private static final Map<Class<?>, Class<?>> REMAP_to_primitive = new HashMap<>() {{
put(Byte.class, byte.class);
put(Short.class, short.class);
put(Integer.class, int.class);
put(Long.class, long.class);
put(Float.class, float.class);
put(Double.class, double.class);
put(Character.class, char.class);
put(Boolean.class, boolean.class);
}};
public static <T> Optional<T> tryConvert(Object input, Class<T> outType) {
T converted = do_convert(input, outType);
return Optional.ofNullable(converted);
}
public static <T> T convert(Object input, Class<T> outType) {
T converted = do_convert(input, outType);
if (converted == null) {
throw new RuntimeException(
"Could not find conversion method\n" + methodName(input.getClass(), outType) +
"\nYou could implement it, or perhaps this is a type of conversion that should not be supported,\n" +
"for example, if it might lose data as a narrowing conversion.");
}
return converted;
}
private static String methodName(Class<?> inType, Class<?> outType) {
return " public static " + REMAP_to_primitive.getOrDefault(outType, outType).getSimpleName() + " to_" +
REMAP_to_primitive.getOrDefault(outType, outType).getSimpleName()
+ "(" + REMAP_to_primitive.getOrDefault(inType, inType).getSimpleName() + " in) {\n" +
" ...\n" +
" }";
}
private static <T> T do_convert(Object input, Class<T> outType) {
// Category 0, nothing to do here
if (outType.equals(input.getClass())) {
return (T) input;
}
if (String.class.isAssignableFrom(outType)) {
return (T) input.toString();
}
// Category 1, happy path, in and out are directly convertible according to JLS assignment
if (outType.isAssignableFrom(input.getClass())) {
return outType.cast(input);
}
// primitive number -> primitive number (ok)
// primitive number -> Boxed Number Type (ok)
// Boxed Number Type -> primitive number (ok)
// Boxed Number Type -> Boxed Number Type (ERROR)
Class<?> loutc = REMAP_to_primitive.getOrDefault(outType, outType);
Class<?> inType = input.getClass();
Class<?> linc = REMAP_to_primitive.getOrDefault(inType, inType);
if (loutc.isPrimitive() && loutc != Boolean.TYPE && loutc != Character.TYPE
&& input instanceof Number) {
if (loutc == long.class) return (T) (Long) ((Number) input).longValue();
if (loutc == int.class) return (T) (Integer) ((Number) input).intValue();
if (loutc == float.class) return (T) (Float) ((Number) input).floatValue();
if (loutc == double.class) return (T) (Double) ((Number) input).doubleValue();
if (loutc == byte.class) return (T) (Byte) ((Number) input).byteValue();
if (loutc == short.class) return (T) (Short) ((Number) input).shortValue();
}
// Category boxing, assignable with auto-(un)boxing, something that Java libs seem to ignore
// This might lead to trouble as this method returns true even when intermediate non-boxed
// types must be used to avoid boxed->boxed conversions
if (ClassUtils.isAssignable(input.getClass(), outType, true)) {
return (T) input;
}
// Last option: custom methods
Method converter = lookup(linc, loutc);
if (converter == null) {
return null;
}
try {
Object result = converter.invoke(null, input);
return (T) result;
} catch (Exception e) {
throw new RuntimeException("Unable to convert (" + input + ") to " + outType.getSimpleName() + ": " + e,e);
}
}
}

View File

@ -0,0 +1,7 @@
package io.nosqlbench.nb.api.config.standard;
/**
* A tagging only interface to indicate sources of static type conversion functions
*/
public interface NBTypeConverters {
}

View File

@ -0,0 +1,114 @@
package io.nosqlbench.nb.api.config.standard;
import java.math.BigDecimal;
public class NBTypeSafeConversions implements NBTypeConverters {
public static BigDecimal to_BigDecimal(String s) {
return BigDecimal.valueOf(Double.parseDouble(s));
}
public static byte to_byte(String s) {
return Byte.parseByte(s);
}
public static char to_char(String s) {
return s.charAt(0);
}
public static int to_int(String s) {
return Integer.parseInt(s);
}
public static short to_short(int in) {
if (in > Short.MAX_VALUE || in < Short.MIN_VALUE) {
throw new RuntimeException("converting " + in + " to short would truncate the value provided.");
}
return (short) in;
}
public static char to_char(int in) {
if (in > Character.MAX_VALUE || in < Character.MIN_VALUE) {
throw new RuntimeException("Converting " + in + " to char would truncate the value provided.");
}
return (char) in;
}
public static int to_int(char in) {
return in;
}
public static short to_short(char in) {
return (short) in;
}
public static byte to_byte(char in) {
int v = in;
if (in > Byte.MAX_VALUE || in < Byte.MIN_VALUE) {
throw new RuntimeException("Converting " + in + " to byte would truncate the value provided.");
}
return (byte) v;
}
public static long to_long(char in) {
return in;
}
public static float to_float(char in) {
return in;
}
public static double to_double(char in) {
return in;
}
public static char to_char(double in) {
if (in > Character.MAX_VALUE || in < Character.MIN_VALUE) {
throw new RuntimeException("Converting " + in + " to char would truncate the value provided.");
}
return (char) in;
}
public static char to_char(short in) {
return (char) in;
}
public static char to_char(long in) {
if (in > Character.MAX_VALUE || in < Character.MIN_VALUE) {
throw new RuntimeException("Converting " + in + " to char would truncate the value provided.");
}
return (char) in;
}
public static char to_char(float in) {
if (in > Character.MAX_VALUE || in < Character.MIN_VALUE) {
throw new RuntimeException("Converting " + in + " to char would truncate the value provided.");
}
return (char) in;
}
public static char to_char(byte in) {
return (char) in;
}
public static double to_double(String in) {
return Double.parseDouble(in);
}
public static short to_short(String in) {
return Short.parseShort(in);
}
public static long to_long(String in) {
return Long.parseLong(in);
}
public static float to_float(String in) {
return Float.parseFloat(in);
}
}

View File

@ -0,0 +1,103 @@
package io.nosqlbench.nb.api.config.standard;
import org.apache.commons.lang3.ClassUtils;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
public class NBTypeConverterTest {
@Test
public void testBasicConversion() {
BigDecimal value = NBTypeConverter.convert("234323433.22", BigDecimal.class);
assertThat(value).isEqualTo(BigDecimal.valueOf(234323433.22d));
}
@Test
public void testCoreTypeClosure() {
for (Class<?> inc : NBTypeConverter.CORE_TYPES) {
for (Class<?> outc : NBTypeConverter.CORE_TYPES) {
Object in = genElement(inc);
System.out.print("inc:" + inc.getSimpleName() + ", outc:" + outc.getSimpleName() +", in:" + in + " --> ");
assertThat(NBTypeConverter.canConvert(in,outc)).as("Should be able to convert core types from " + inc.getSimpleName() + " to " + outc);
Object out = NBTypeConverter.convert(in, outc);
System.out.println("out:" + out +", type:" + out.getClass().getSimpleName());
assertThat(ClassUtils.isAssignable(out.getClass(),outc,true))
.as(outc.getSimpleName() + " should be assignable from "+ out.getClass().getSimpleName())
.isTrue();
}
}
System.out.println();
}
@Test
public void testNumberToPrimitiveInterop() {
String s = NBTypeConverter.convert(Character.valueOf('1'),String.class);
Character cb = NBTypeConverter.convert(7,Character.class);
Short b = NBTypeConverter.convert(3,Short.class);
short a = NBTypeConverter.convert(3,short.class);
short c = NBTypeConverter.convert(Integer.valueOf(3),short.class);
Short d = NBTypeConverter.convert(Integer.valueOf(3),Short.class);
}
@Test
public void testAssignables() {
// long l3 = (int) 3;
// int i3 = (long) 3l;
assertThat(long.class.isAssignableFrom(Long.class)).isFalse();
assertThat(Long.class.isAssignableFrom(long.class)).isFalse();
assertThat(long.class.isAssignableFrom(long.class)).isTrue();
assertThat(Long.class.isAssignableFrom(Long.class)).isTrue();
}
// @Test
// public void testUnboxing() {
// Double d = (Double) (Integer) 3;
// Object o = NBTypeConverter.adapt(5,double.class);
// double v = NBTypeConverter.adapt(5,double.class);
// }
public Object genElement(Class<?> type) {
String typeName = type.getSimpleName();
switch (typeName) {
case "byte":
return 1;
case "Byte":
return Byte.valueOf("2");
case "short":
return 3;
case "Short":
return Short.valueOf("4");
case "int":
return 5;
case "Integer":
return Integer.valueOf("6");
case "long":
return 7L;
case "Long":
return Long.valueOf(8L);
case "float":
return 9.0f;
case "Float":
return Float.valueOf(9.1f);
case "double":
return 10.0d;
case "Double":
return Double.valueOf(10.1d);
case "Character":
return Character.valueOf('c');
case "char":
return 'c';
case "String":
return "1";
default:
throw new RuntimeException("Unknown type:" + typeName);
}
}
}