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:
parent
b76afa38bb
commit
b227f0bccc
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user