mirror of
https://github.com/nosqlbench/nosqlbench.git
synced 2025-02-25 18:55:28 -06:00
add universal type converter for config
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -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 {
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user