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 14 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 @@ -68,6 +68,8 @@ private POJODefinition _introspectDefinition(Class<?> beanType,
} else if (argType == Long.class || argType == Long.TYPE) {
constructors.addLongConstructor(ctor);
}
} else if (RecordsHelpers.isRecordConstructor(beanType, ctor, propsByName)) {
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
constructors.addRecordConstructor(ctor);
}
}
}
Expand Down Expand Up @@ -152,7 +154,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 +202,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,35 @@ 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.
*/
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 +84,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,10 @@ public class BeanReader
*/
protected final BeanConstructors _constructors;

protected final boolean _isRecordType;

//protected final Map<String, Integer> propertyPositions = new HashMap<>();
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved

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

/*
props.values().forEach(prop -> {
propertyPositions.put(prop.getName(), prop.getIndex());
});
*/
}

@Deprecated // since 2.17
Expand Down Expand Up @@ -104,6 +115,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 +134,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 +149,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 +185,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
Expand Up @@ -8,8 +8,7 @@
import java.util.Map;

/**
* 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
*/
Expand Down Expand Up @@ -39,7 +38,8 @@ public final class RecordsHelpers {
}
private RecordsHelpers() {}

static boolean isRecordConstructor(Class<?> beanClass, Constructor<?> ctor, Map<String, PropBuilder> propsByName) {
static boolean isRecordConstructor(Class<?> beanClass, Constructor<?> ctor,
Map<String, PropBuilder> propsByName) {
if (!supportsRecords || !isRecordType(beanClass)) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ protected BeanReader _resolveBeanForDeser(Class<?> raw, POJODefinition beanDef)
final Map<String, BeanPropertyReader> propMap;
Map<String, String> aliasMapping = null;

boolean isRecord = RecordsHelpers.isRecordType(raw);
if (len == 0) {
propMap = Collections.emptyMap();
} else {
Expand All @@ -473,22 +474,31 @@ protected BeanReader _resolveBeanForDeser(Class<?> raw, POJODefinition beanDef)
setter = null;
}
}
// if no setter, field would do as well
if (setter == null) {
if (field == null) {
continue;
if (isRecord) {
try {
field = raw.getDeclaredField(rawProp.name);
} catch (NoSuchFieldException e) {
throw new IllegalStateException("Cannot access field " + rawProp.name
+ " of record class " + raw.getName(), e);
}
// fields should always be public, but let's just double-check
if (forceAccess) {
field.setAccessible(true);
} else if (!Modifier.isPublic(field.getModifiers())) {
continue;
} else {
// if no setter, field would do as well
if (setter == null) {
if (field == null) {
continue;
}
// fields should always be public, but let's just double-check
if (forceAccess) {
field.setAccessible(true);
} else if (!Modifier.isPublic(field.getModifiers())) {
continue;
}
}
}

propMap.put(rawProp.name, new BeanPropertyReader(rawProp.name, field, setter));
propMap.put(rawProp.name, new BeanPropertyReader(rawProp.name, field, setter, i));

// 25-Jan-2020, tatu: Aliases are bit different because we can not tie them into
// 25-Jan-2020, tatu: Aliases are a bit different because we can not tie them into
// specific reader instance, due to resolution of cyclic dependencies. Instead,
// we must link via name of primary property, unfortunately:
if (rawProp.hasAliases()) {
Expand Down
Loading