Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add record support #163

Merged
merged 20 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved

/**
* @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.
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
*
* @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