Skip to content

Commit

Permalink
Add record support (#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomaszGaweda authored Jul 1, 2024
1 parent 32eb3e1 commit 26379a6
Show file tree
Hide file tree
Showing 10 changed files with 381 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ public class BeanConstructors

protected Constructor<?> _noArgsCtor;

/**
* Constructor (canonical) used when deserializing Java Record types.
*
* @since 2.18
*/
protected Constructor<?> _recordCtor;

protected Constructor<?> _intCtor;
protected Constructor<?> _longCtor;
protected Constructor<?> _stringCtor;
Expand All @@ -27,6 +34,14 @@ public BeanConstructors addNoArgsConstructor(Constructor<?> ctor) {
return this;
}

/**
* @since 2.18
*/
public BeanConstructors addRecordConstructor(Constructor<?> ctor) {
_recordCtor = ctor;
return this;
}

public BeanConstructors addIntConstructor(Constructor<?> ctor) {
_intCtor = ctor;
return this;
Expand All @@ -46,6 +61,9 @@ public void forceAccess() {
if (_noArgsCtor != null) {
_noArgsCtor.setAccessible(true);
}
if (_recordCtor != null) {
_recordCtor.setAccessible(true);
}
if (_intCtor != null) {
_intCtor.setAccessible(true);
}
Expand All @@ -64,6 +82,16 @@ protected Object create() throws Exception {
return _noArgsCtor.newInstance((Object[]) null);
}

/**
* @since 2.18
*/
protected Object createRecord(Object[] components) throws Exception {
if (_recordCtor == null) {
throw new IllegalStateException("Class "+_valueType.getName()+" does not have record constructor to use");
}
return _recordCtor.newInstance(components);
}

protected Object create(String str) throws Exception {
if (_stringCtor == null) {
throw new IllegalStateException("Class "+_valueType.getName()+" does not have single-String constructor to use");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,27 @@ private POJODefinition _introspectDefinition(Class<?> beanType,
constructors = null;
} else {
constructors = new BeanConstructors(beanType);
for (Constructor<?> ctor : beanType.getDeclaredConstructors()) {
Class<?>[] argTypes = ctor.getParameterTypes();
if (argTypes.length == 0) {
constructors.addNoArgsConstructor(ctor);
} else if (argTypes.length == 1) {
Class<?> argType = argTypes[0];
if (argType == String.class) {
constructors.addStringConstructor(ctor);
} else if (argType == Integer.class || argType == Integer.TYPE) {
constructors.addIntConstructor(ctor);
} else if (argType == Long.class || argType == Long.TYPE) {
constructors.addLongConstructor(ctor);
if (RecordsHelpers.isRecordType(beanType)) {
Constructor<?> canonical = RecordsHelpers.findCanonicalConstructor(beanType);
if (canonical == null) { // should never happen
throw new IllegalArgumentException(
"Unable to find canonical constructor of Record type `"+beanType.getClass().getName()+"`");
}
constructors.addRecordConstructor(canonical);
} else {
for (Constructor<?> ctor : beanType.getDeclaredConstructors()) {
Class<?>[] argTypes = ctor.getParameterTypes();
if (argTypes.length == 0) {
constructors.addNoArgsConstructor(ctor);
} else if (argTypes.length == 1) {
Class<?> argType = argTypes[0];
if (argType == String.class) {
constructors.addStringConstructor(ctor);
} else if (argType == Integer.class || argType == Integer.TYPE) {
constructors.addIntConstructor(ctor);
} else if (argType == Long.class || argType == Long.TYPE) {
constructors.addLongConstructor(ctor);
}
}
}
}
Expand Down Expand Up @@ -152,7 +161,7 @@ private static void _introspect(Class<?> currType, Map<String, PropBuilder> prop
name = decap(name.substring(2));
_propFrom(props, name).withIsGetter(m);
}
} else if (isFieldNameGettersEnabled){
} else if (isFieldNameGettersEnabled) {
// 10-Mar-2024: [jackson-jr#94]:
// This will allow getters with field name as their getters,
// like the ones generated by Groovy (or JDK 17 for Records).
Expand Down Expand Up @@ -200,8 +209,10 @@ private static String decap(String name) {

/**
* Helper method to detect Groovy's problematic metadata accessor type.
* Groovy MetaClass have cyclic reference, and hence the class containing it should not be
* serialized without either removing that reference, or skipping over such references.
*<p>
* NOTE: Groovy MetaClass have cyclic reference, and hence the class containing
* it should not be serialized without either removing that reference,
* or skipping over such references.
*/
protected static boolean isGroovyMetaClass(Class<?> clazz) {
return "groovy.lang.MetaClass".equals(clazz.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,37 @@ public final class BeanPropertyReader
*/
private final Field _field;

public BeanPropertyReader(String name, Field f, Method setter) {
/**
* Index used for {@code Record}s constructor parameters. It is not used for getter/setter methods.
*
* @since 2.18
*/
private final int _index;

/**
* @since 2.18
*/
public BeanPropertyReader(String name, Field f, Method setter, int propertyIndex) {
if ((f == null) && (setter == null)) {
throw new IllegalArgumentException("Both `field` and `setter` can not be null");
}
_name = name;
_field = f;
_setter = setter;
_valueReader = null;
_index = propertyIndex;
}

@Deprecated // @since 2.18
public BeanPropertyReader(String name, Field f, Method setter) {
this(name, f, setter, -1);
}

protected BeanPropertyReader(BeanPropertyReader src, ValueReader vr) {
_name = src._name;
_field = src._field;
_setter = src._setter;
_index = src._index;
_valueReader = vr;
}

Expand All @@ -69,6 +86,13 @@ public Class<?> rawSetterType() {
public ValueReader getReader() { return _valueReader; }
public String getName() { return _name; }

/**
* @since 2.18
*/
public int getIndex() {
return _index;
}

public void setValueFor(Object bean, Object[] valueBuf)
throws IOException
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class BeanReader
*/
protected final BeanConstructors _constructors;

protected final boolean _isRecordType;

/**
* Constructors used for deserialization use case
*
Expand All @@ -56,6 +58,7 @@ public BeanReader(Class<?> type, Map<String, BeanPropertyReader> props,
aliasMapping = Collections.emptyMap();
}
_aliasMapping = aliasMapping;
_isRecordType = RecordsHelpers.isRecordType(type);
}

@Deprecated // since 2.17
Expand All @@ -69,12 +72,6 @@ public BeanReader(Class<?> type, Map<String, BeanPropertyReader> props,
ignorableNames, aliasMapping);
}

@Deprecated // since 2.11
public BeanReader(Class<?> type, Map<String, BeanPropertyReader> props,
Constructor<?> defaultCtor, Constructor<?> stringCtor, Constructor<?> longCtor) {
this(type, props, defaultCtor, stringCtor, longCtor, null, null);
}

public Map<String,BeanPropertyReader> propertiesByName() { return _propsByName; }

public BeanPropertyReader findProperty(String name) {
Expand Down Expand Up @@ -104,6 +101,9 @@ public Object readNext(JSONReader r, JsonParser p) throws IOException
return _constructors.create(p.getLongValue());
case START_OBJECT:
{
if (_isRecordType) {
return readRecord(r, p);
}
Object bean = _constructors.create();
final Object[] valueBuf = r._setterBuffer;
String propName;
Expand All @@ -120,7 +120,7 @@ public Object readNext(JSONReader r, JsonParser p) throws IOException
// also verify we are not confused...
if (!p.hasToken(JsonToken.END_OBJECT)) {
throw _reportProblem(p);
}
}
return bean;
}
default:
Expand All @@ -135,7 +135,23 @@ public Object readNext(JSONReader r, JsonParser p) throws IOException
throw JSONObjectException.from(p,
"Can not create a "+_valueType.getName()+" instance out of "+_tokenDesc(p));
}


private Object readRecord(JSONReader r, JsonParser p) throws Exception {
final Object[] values = new Object[propertiesByName().size()];

String propName;
for (; (propName = p.nextFieldName()) != null;) {
BeanPropertyReader prop = findProperty(propName);
if (prop == null) {
handleUnknown(r, p, propName);
continue;
}
Object value = prop.getReader().readNext(r, p);
values[prop.getIndex()] = value;
}
return _constructors.createRecord(values);
}

/**
* Method used for deserialization; will read an instance of the bean
* type using given parser.
Expand All @@ -155,6 +171,9 @@ public Object read(JSONReader r, JsonParser p) throws IOException
return _constructors.create(p.getLongValue());
case START_OBJECT:
{
if (_isRecordType) {
return readRecord(r, p);
}
Object bean = _constructors.create();
String propName;
final Object[] valueBuf = r._setterBuffer;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,63 @@
package com.fasterxml.jackson.jr.ob.impl;

import com.fasterxml.jackson.jr.ob.impl.POJODefinition.PropBuilder;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;

import com.fasterxml.jackson.jr.ob.impl.POJODefinition.PropBuilder;

/**
* Helper class to get Java Record metadata, from Java 8 (not using
* JDK 17 methods)
* Helper class to get Java Record metadata.
*
* @since 2.18
*/
public final class RecordsHelpers {
private static boolean supportsRecords;

private static Method getRecordComponentsMethod;
private static Method getTypeMethod;
private static Method getComponentTypeMethod;

static {
Method getRecordComponentsMethod;
Method getTypeMethod;
// We may need this in future:
//private static Method getComponentNameMethod;

static {
try {
getRecordComponentsMethod = Class.class.getMethod("getRecordComponents");
Class<?> recordComponentClass = Class.forName("java.lang.reflect.RecordComponent");
getTypeMethod = recordComponentClass.getMethod("getType");
getComponentTypeMethod = recordComponentClass.getMethod("getType");
//getComponentNameMethod = recordComponentClass.getMethod("getName");
supportsRecords = true;
} catch (Throwable t) {
getRecordComponentsMethod = null;
getTypeMethod = null;
supportsRecords = false;
}

RecordsHelpers.getRecordComponentsMethod = getRecordComponentsMethod;
RecordsHelpers.getTypeMethod = getTypeMethod;
}
private RecordsHelpers() {}

static boolean isRecordConstructor(Class<?> beanClass, Constructor<?> ctor, Map<String, PropBuilder> propsByName) {
static Constructor<?> findCanonicalConstructor(Class<?> beanClass) {
// sanity check: caller shouldn't rely on it
if (!supportsRecords || !isRecordType(beanClass)) {
return null;
}
try {
final Class<?>[] componentTypes = componentTypes(beanClass);
for (Constructor<?> ctor : beanClass.getDeclaredConstructors()) {
final Class<?>[] parameterTypes = ctor.getParameterTypes();
if (parameterTypes.length == componentTypes.length) {
if (Arrays.equals(parameterTypes, componentTypes)) {
return ctor;
}
}
}
} catch (ReflectiveOperationException e) {
;
}
return null;
}

static boolean isRecordConstructor(Class<?> beanClass, Constructor<?> ctor,
Map<String, PropBuilder> propsByName)
{
if (!supportsRecords || !isRecordType(beanClass)) {
return false;
}
Expand All @@ -50,27 +68,28 @@ static boolean isRecordConstructor(Class<?> beanClass, Constructor<?> ctor, Map<
}

try {
Object[] recordComponents = (Object[]) getRecordComponentsMethod.invoke(beanClass);
Class<?>[] componentTypes = new Class<?>[recordComponents.length];
for (int i = 0; i < recordComponents.length; i++) {
Object recordComponent = recordComponents[i];
Class<?> type = (Class<?>) getTypeMethod.invoke(recordComponent);
componentTypes[i] = type;
}

for (int i = 0; i < parameterTypes.length; i++) {
if (parameterTypes[i] != componentTypes[i]) {
return false;
}
}
} catch (IllegalAccessException | InvocationTargetException e) {
Class<?>[] componentTypes = componentTypes(beanClass);
return Arrays.equals(parameterTypes, componentTypes);
} catch (ReflectiveOperationException e) {
return false;
}
return true;
}

static boolean isRecordType(Class<?> cls) {
Class<?> parent = cls.getSuperclass();
return (parent != null) && "java.lang.Record".equals(parent.getName());
}

private static Class<?>[] componentTypes(Class<?> recordType)
throws ReflectiveOperationException
{
Object[] recordComponents = (Object[]) getRecordComponentsMethod.invoke(recordType);
Class<?>[] componentTypes = new Class<?>[recordComponents.length];
for (int i = 0; i < recordComponents.length; i++) {
Object recordComponent = recordComponents[i];
Class<?> type = (Class<?>) getComponentTypeMethod.invoke(recordComponent);
componentTypes[i] = type;
}
return componentTypes;
}
}
Loading

0 comments on commit 26379a6

Please sign in to comment.