ValueClassConverter.java
package com.surrealdb;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
class ValueClassConverter<T> {
// Reflective access to record APIs (JDK 16+), looked up once.
// The library is compiled with --release 8, so we cannot reference
// Class.isRecord(),
// Class.getRecordComponents() or java.lang.reflect.RecordComponent directly.
private static final Method IS_RECORD;
private static final Method GET_RECORD_COMPONENTS;
private static final Method RC_GET_NAME;
private static final Method RC_GET_TYPE;
private static final Method RC_GET_GENERIC_TYPE;
static {
Method isRecord = null;
Method getRecordComponents = null;
Method rcGetName = null;
Method rcGetType = null;
Method rcGetGenericType = null;
try {
isRecord = Class.class.getMethod("isRecord");
getRecordComponents = Class.class.getMethod("getRecordComponents");
final Class<?> rcClass = Class.forName("java.lang.reflect.RecordComponent");
rcGetName = rcClass.getMethod("getName");
rcGetType = rcClass.getMethod("getType");
rcGetGenericType = rcClass.getMethod("getGenericType");
} catch (ReflectiveOperationException ignored) {
// Pre-16 JVM: leave everything null and fall through to the POJO path.
}
IS_RECORD = isRecord;
GET_RECORD_COMPONENTS = getRecordComponents;
RC_GET_NAME = rcGetName;
RC_GET_TYPE = rcGetType;
RC_GET_GENERIC_TYPE = rcGetGenericType;
}
private final Class<T> clazz;
ValueClassConverter(Class<T> clazz) {
this.clazz = clazz;
}
private static boolean isRecord(Class<?> clazz) {
if (IS_RECORD == null) {
return false;
}
try {
return (Boolean) IS_RECORD.invoke(clazz);
} catch (ReflectiveOperationException e) {
return false;
}
}
private static java.lang.Object convertSingleValue(final Value value) {
if (value.isNull())
return null;
if (value.isBoolean())
return value.getBoolean();
if (value.isDouble())
return value.getDouble();
if (value.isLong())
return value.getLong();
if (value.isString())
return value.getString();
if (value.isRecordId())
return value.getRecordId();
if (value.isGeometry())
return value.getGeometry();
if (value.isBigDecimal())
return value.getBigDecimal();
if (value.isBytes())
return value.getBytes();
if (value.isUuid())
return value.getUuid();
if (value.isDuration())
return value.getDuration();
if (value.isDateTime())
return value.getDateTime();
throw new SurrealException("Unsupported value: " + value);
}
// Type-aware single-value conversion: returns the boxed Java object that
// Field.set / Constructor.newInstance will auto-unbox into the target slot.
private static java.lang.Object convertSingleValueTyped(final Value value, final Class<?> type) {
if (value.isNull()) {
return null;
}
if (value.isDouble()) {
final double d = value.getDouble();
if (type == Float.TYPE || type == Float.class) {
return (float) d;
}
return d;
}
if (value.isLong()) {
final long l = value.getLong();
if (type == Integer.TYPE || type == Integer.class) {
return (int) l;
}
if (type == Short.TYPE || type == Short.class) {
return (short) l;
}
return l;
}
if (value.isRecordId()) {
if (type == Id.class) {
return value.getRecordId().getId();
}
return value.getRecordId();
}
return convertSingleValue(value);
}
// Convert a Value into the Java object suitable for the given declared type.
// Used by both the POJO field-setting path and the record canonical-constructor
// path.
private static java.lang.Object convertValueToType(final Value value, final Class<?> type, final Type genericType)
throws ReflectiveOperationException {
if (Value.class.equals(type)) {
return value;
}
if (value.isArray()) {
final Class<?> elementType = firstTypeArgumentRaw(genericType);
final Type elementGenericType = firstTypeArgument(genericType);
final List<java.lang.Object> list = new ArrayList<>();
for (final Value element : value.getArray()) {
list.add(convertArrayElement(element, elementType, elementGenericType));
}
if (type == byte[].class) {
final byte[] bytes = new byte[list.size()];
for (int i = 0; i < list.size(); i++) {
final java.lang.Object e = list.get(i);
if (e instanceof Number) {
bytes[i] = ((Number) e).byteValue();
} else {
throw new SurrealException(
"Cannot convert " + (e == null ? "null" : e.getClass()) + " to byte");
}
}
return bytes;
}
if (Optional.class.equals(type)) {
return Optional.of(list);
}
return list;
}
if (value.isObject()) {
if (Map.class.isAssignableFrom(type)) {
final Class<?> valueType = secondTypeArgumentRaw(genericType);
if (valueType == null) {
throw new SurrealException("Unsupported map type for: " + genericType);
}
final Map<String, java.lang.Object> map = new HashMap<>();
for (final Entry mapEntry : value.getObject()) {
final String entryKey = mapEntry.getKey();
final Value entryValue = mapEntry.getValue();
// todo - array support inside maps
if (entryValue.isObject()) {
map.put(entryKey, convert(valueType, entryValue.getObject()));
} else {
map.put(entryKey, convertSingleValue(entryValue));
}
}
return map;
}
if (Optional.class.equals(type)) {
final Class<?> innerType = firstTypeArgumentRaw(genericType);
if (innerType == null) {
throw new SurrealException("Unsupported Optional type for: " + genericType);
}
return Optional.of(convert(innerType, value.getObject()));
}
return convert(type, value.getObject());
}
// scalar value
if (Optional.class.equals(type)) {
final Class<?> innerType = firstTypeArgumentRaw(genericType);
final java.lang.Object converted = innerType == null
? convertSingleValue(value)
: convertSingleValueTyped(value, innerType);
return converted == null ? Optional.empty() : Optional.of(converted);
}
return convertSingleValueTyped(value, type);
}
private static java.lang.Object convertArrayElement(final Value value, final Class<?> elementType,
final Type elementGenericType) throws ReflectiveOperationException {
if (value.isObject()) {
if (elementType == null) {
throw new SurrealException("Unsupported element type for array");
}
return convert(elementType, value.getObject());
}
if (value.isArray()) {
final List<java.lang.Object> nested = new ArrayList<>();
for (final Value v : value.getArray()) {
nested.add(convertArrayElement(v, elementType, elementGenericType));
}
return nested;
}
// Preserve the historical behaviour: scalar array elements are type-agnostic.
return convertSingleValue(value);
}
private static Type firstTypeArgument(final Type genericType) {
if (genericType instanceof ParameterizedType) {
final Type[] args = ((ParameterizedType) genericType).getActualTypeArguments();
if (args.length > 0) {
return args[0];
}
}
return null;
}
private static Class<?> firstTypeArgumentRaw(final Type genericType) {
return rawType(firstTypeArgument(genericType));
}
private static Class<?> secondTypeArgumentRaw(final Type genericType) {
if (genericType instanceof ParameterizedType) {
final Type[] args = ((ParameterizedType) genericType).getActualTypeArguments();
if (args.length > 1) {
return rawType(args[1]);
}
}
return null;
}
private static Class<?> rawType(final Type type) {
if (type instanceof Class) {
return (Class<?>) type;
}
if (type instanceof ParameterizedType) {
final Type raw = ((ParameterizedType) type).getRawType();
if (raw instanceof Class) {
return (Class<?>) raw;
}
}
return null;
}
private static <T> T convert(Class<T> clazz, Object source) throws ReflectiveOperationException {
if (isRecord(clazz)) {
return convertRecord(clazz, source);
}
final T target = clazz.getConstructor().newInstance();
initOptionalFields(clazz, target);
for (final Entry entry : source) {
try {
final String key = entry.getKey();
final Value value = entry.getValue();
final Field field = getInheritedDeclaredField(clazz, key);
field.setAccessible(true);
final java.lang.Object converted = convertValueToType(value, field.getType(), field.getGenericType());
if (converted == null && field.getType().isPrimitive()) {
// Leave primitive fields at their default value; setting null would throw.
continue;
}
field.set(target, converted);
} catch (NoSuchFieldException e) {
// Safe to ignore: source has a key with no matching field.
}
}
return target;
}
private static <T> T convertRecord(final Class<T> clazz, final Object source) throws ReflectiveOperationException {
if (GET_RECORD_COMPONENTS == null) {
// Should be unreachable: isRecord() returned true, so the JVM exposes
// RecordComponent.
throw new SurrealException("Record reflection APIs unavailable for " + clazz.getName());
}
final java.lang.Object[] componentsArray = (java.lang.Object[]) GET_RECORD_COMPONENTS.invoke(clazz);
final int count = componentsArray.length;
final String[] names = new String[count];
final Class<?>[] types = new Class<?>[count];
final Type[] genericTypes = new Type[count];
for (int i = 0; i < count; i++) {
final java.lang.Object rc = componentsArray[i];
names[i] = (String) RC_GET_NAME.invoke(rc);
types[i] = (Class<?>) RC_GET_TYPE.invoke(rc);
genericTypes[i] = (Type) RC_GET_GENERIC_TYPE.invoke(rc);
}
// Build a lookup of incoming entries keyed by name.
final Map<String, Value> entries = new HashMap<>();
for (final Entry entry : source) {
entries.put(entry.getKey(), entry.getValue());
}
final java.lang.Object[] args = new java.lang.Object[count];
for (int i = 0; i < count; i++) {
final Value value = entries.get(names[i]);
final Class<?> type = types[i];
if (value == null) {
args[i] = defaultForRecordComponent(type);
continue;
}
final java.lang.Object converted = convertValueToType(value, type, genericTypes[i]);
if (converted == null) {
args[i] = defaultForRecordComponent(type);
} else {
args[i] = converted;
}
}
final Constructor<T> ctor = clazz.getDeclaredConstructor(types);
ctor.setAccessible(true);
return ctor.newInstance(args);
}
private static java.lang.Object defaultForRecordComponent(final Class<?> type) {
if (Optional.class.equals(type)) {
return Optional.empty();
}
if (type.isPrimitive()) {
if (type == Boolean.TYPE)
return Boolean.FALSE;
if (type == Byte.TYPE)
return (byte) 0;
if (type == Short.TYPE)
return (short) 0;
if (type == Integer.TYPE)
return 0;
if (type == Long.TYPE)
return 0L;
if (type == Float.TYPE)
return 0f;
if (type == Double.TYPE)
return 0d;
if (type == Character.TYPE)
return (char) 0;
}
return null;
}
private static <T> void initOptionalFields(Class<?> clazz, T target) throws IllegalAccessException {
Class<?> c = clazz;
while (c != null && c != java.lang.Object.class) {
for (final Field field : c.getDeclaredFields()) {
int mods = field.getModifiers();
if (Modifier.isStatic(mods) || Modifier.isTransient(mods)) {
continue;
}
if (Optional.class.equals(field.getType())) {
field.setAccessible(true);
if (field.get(target) == null) {
field.set(target, Optional.empty());
}
}
}
c = c.getSuperclass();
}
}
static Field getInheritedDeclaredField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
while (clazz != null) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy.");
}
final T convert(final Value value) {
try {
if (value.isNone() || value.isNull())
return null;
if (!value.isObject())
throw new SurrealException("Unexpected value: " + value);
return convert(clazz, value.getObject());
} catch (ReflectiveOperationException e) {
throw new SurrealException("Failed to create instance of " + clazz.getName(), e);
}
}
}