Skip to content

Commit

Permalink
Merge branch '2.18'
Browse files Browse the repository at this point in the history
  • Loading branch information
cowtowncoder committed Jun 13, 2024
2 parents 5377d70 + a62264f commit 0a517e9
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public class BeanConstructors

protected Constructor<?> _noArgsCtor;

// @since 2.18
protected Constructor<?> _recordCtor;

protected Constructor<?> _intCtor;
protected Constructor<?> _longCtor;
protected Constructor<?> _stringCtor;
Expand All @@ -27,6 +30,12 @@ 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 +55,9 @@ public void forceAccess() {
if (_noArgsCtor != null) {
_noArgsCtor.setAccessible(true);
}
if (_recordCtor != null) {
_recordCtor.setAccessible(true);
}
if (_intCtor != null) {
_intCtor.setAccessible(true);
}
Expand All @@ -63,6 +75,14 @@ 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ private POJODefinition _introspectDefinition(Class<?> beanType,
} else if (argType == Long.class || argType == Long.TYPE) {
constructors.addLongConstructor(ctor);
}
} else if (RecordsHelpers.isRecordConstructor(beanType, ctor, propsByName)) {
constructors.addRecordConstructor(ctor);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class BeanReader

protected final BeanConstructors _constructors;

protected final boolean _isRecordType;

// // 13-Dec-2017, tatu: NOTE! These will be constructed right after construction, but
// // not during it (due to need to resolve possible cyclic deps). So they are
// // non-final due to this but never `null` before use.
Expand All @@ -52,6 +54,7 @@ protected BeanReader(Class<?> type, Map<String,BeanPropertyReader> propsByName,
_ignorableNames = ignorableNames;
_aliasMapping = aliasMapping;
_caseInsensitive = caseInsensitive;
_isRecordType = RecordsHelpers.isRecordType(type);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package tools.jackson.jr.ob.impl;

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

import tools.jackson.jr.ob.impl.POJODefinition.PropBuilder;

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

private static Method getRecordComponentsMethod;
private static Method getTypeMethod;

static {
Method getRecordComponentsMethod;
Method getTypeMethod;

try {
getRecordComponentsMethod = Class.class.getMethod("getRecordComponents");
Class<?> recordComponentClass = Class.forName("java.lang.reflect.RecordComponent");
getTypeMethod = recordComponentClass.getMethod("getType");
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) {
if (!supportsRecords || !isRecordType(beanClass)) {
return false;
}

Class<?>[] parameterTypes = ctor.getParameterTypes();
if (parameterTypes.length != propsByName.size()) {
return false;
}

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) {
return false;
}
return true;
}

static boolean isRecordType(Class<?> cls) {
Class<?> parent = cls.getSuperclass();
return (parent != null) && "java.lang.Record".equals(parent.getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -472,31 +472,40 @@ protected BeanReader _resolveBeanForDeser(Class<?> raw, POJODefinition beanDef)
final boolean useFields = JSON.Feature.USE_FIELDS.isEnabled(_features);
for (int i = 0; i < len; ++i) {
POJODefinition.Prop rawProp = rawProps.get(i);
Method m = rawProp.setter;
Field f = useFields ? rawProp.field : null;
Method setter = rawProp.setter;
Field field = useFields ? rawProp.field : null;

if (m != null) {
if (setter != null) {
if (forceAccess) {
m.setAccessible(true);
} else if (!Modifier.isPublic(m.getModifiers())) {
setter.setAccessible(true);
} else if (!Modifier.isPublic(setter.getModifiers())) {
// access to non-public setters must be forced to be usable:
m = null;
setter = null;
}
}
// if no setter, field would do as well
if (m == null) {
if (f == null) {
continue;
if (RecordsHelpers.isRecordType(raw)) {
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) {
f.setAccessible(true);
} else if (!Modifier.isPublic(f.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, f, m));
propMap.put(rawProp.name, new BeanPropertyReader(rawProp.name, field, setter));

// 25-Jan-2020, tatu: Aliases are bit different because we can not tie them into
// specific reader instance, due to resolution of cyclic dependencies. Instead,
Expand Down
9 changes: 8 additions & 1 deletion jr-test-module/src/test/java/Java17RecordTest.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import com.fasterxml.jackson.jr.ob.JSON;
import com.fasterxml.jackson.jr.ob.JSON.Feature;
import org.junit.Assert;
import org.junit.Test;

Expand All @@ -12,9 +13,15 @@ public class Java17RecordTest {

@Test
public void testJava14RecordSupport() throws IOException {
JSON jsonParser = JSON.builder().enable(Feature.USE_FIELD_MATCHING_GETTERS).build();
var expectedString = "{\"message\":\"MOO\",\"object\":{\"Foo\":\"Bar\"}}";
var json = JSON.builder().enable(JSON.Feature.USE_FIELD_MATCHING_GETTERS).build().asString(new Cow("MOO", Map.of("Foo", "Bar")));
Cow expectedObject = new Cow("MOO", Map.of("Foo", "Bar"));

var json = jsonParser.asString(expectedObject);
Assert.assertEquals(expectedString, json);

Cow object = jsonParser.beanFrom(Cow.class, json);
Assert.assertEquals(expectedObject, object);
}

record Cow(String message, Map<String, String> object) {
Expand Down
4 changes: 4 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,7 @@ Julian Honnen (@jhonnen)
when using `JSON.treeFrom()`
(2.17.1)

Tomasz Gawęda (@TomaszGaweda)

* Contributed #148: Add support for Java Record deserialization
(2.18.0)
3 changes: 2 additions & 1 deletion release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ Modules:

2.18.0 (not yet released)

No changes since 2.17
#148: Add support for Java Record deserialization
(contributed by Tomasz G)

2.17.1 (04-May-2024)

Expand Down

0 comments on commit 0a517e9

Please sign in to comment.