Skip to content

Commit

Permalink
Getting close to completing #2709, now basic deserialization works; s…
Browse files Browse the repository at this point in the history
…till need overrides
  • Loading branch information
cowtowncoder committed Aug 21, 2020
1 parent 963f34d commit 263b544
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;

import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonIncludeProperties;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.JsonCreator.Mode;

import com.fasterxml.jackson.core.JsonParser;
Expand All @@ -29,6 +24,7 @@
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.ext.OptionalHandlerFactory;
import com.fasterxml.jackson.databind.introspect.*;
import com.fasterxml.jackson.databind.jdk14.JDK14Util;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
Expand Down Expand Up @@ -278,6 +274,15 @@ protected ValueInstantiator _constructDefaultValueInstantiator(DeserializationCo
_addDeserializerFactoryMethods(ctxt, beanDesc, vchecker, intr, creators, creatorDefs);
// constructors only usable on concrete types:
if (beanDesc.getType().isConcrete()) {
// [databind#2709]: Record support
if (beanDesc.getType().isRecordType()) {
final List<String> names = new ArrayList<>();
AnnotatedConstructor canonical = JDK14Util.findRecordConstructor(ctxt, beanDesc, names);
if (canonical != null) {
_addRecordConstructor(ctxt, beanDesc, creators, canonical, names);
return creators.constructValueInstantiator(ctxt);
}
}
_addDeserializerConstructors(ctxt, beanDesc, vchecker, intr, creators, creatorDefs);
}
return creators.constructValueInstantiator(ctxt);
Expand Down Expand Up @@ -542,6 +547,30 @@ protected void _addDeserializerConstructors(DeserializationContext ctxt,
}
}

/**
* Helper method called when a {@code java.lang.Record} definition's "canonical"
* constructor is to be used: if so, we have implicit names to consider.
*
* @since 2.12
*/
protected void _addRecordConstructor(DeserializationContext ctxt,
BeanDescription beanDesc, CreatorCollector creators,
AnnotatedConstructor canonical, List<String> names)
throws JsonMappingException
{
final int argCount = canonical.getParameterCount();
final AnnotationIntrospector intr = ctxt.getAnnotationIntrospector();
final SettableBeanProperty[] properties = new SettableBeanProperty[argCount];

for (int i = 0; i < argCount; ++i) {
final AnnotatedParameter param = canonical.getParameter(i);
JacksonInject.Value injectable = intr.findInjectableValue(param);
final PropertyName name = PropertyName.construct(names.get(i));
properties[i] = constructCreatorProperty(ctxt, beanDesc, name, i, param, injectable);
}
creators.addPropertyCreator(canonical, false, properties);
}

/**
* Helper method called when there is the explicit "is-creator" with mode of "delegating"
*
Expand Down Expand Up @@ -1038,7 +1067,7 @@ protected SettableBeanProperty constructCreatorProperty(DeserializationContext c

private PropertyName _findParamName(AnnotatedParameter param, AnnotationIntrospector intr)
{
if (param != null && intr != null) {
if (intr != null) {
PropertyName name = intr.findNameForDeserialization(param);
if (name != null) {
return name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public PropertyName explicitParamName(int i) {
}
return null;
}

public PropertyName findImplicitParamName(int i) {
String str = _intr.findImplicitPropertyName(_params[i].annotated);
if (str != null && !str.isEmpty()) {
Expand Down
134 changes: 134 additions & 0 deletions src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
package com.fasterxml.jackson.databind.jdk14;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonCreator.Mode;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.impl.CreatorCollector;
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
import com.fasterxml.jackson.databind.introspect.AnnotatedWithParams;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
import com.fasterxml.jackson.databind.util.ClassUtil;

/**
Expand All @@ -18,6 +32,12 @@ public static String[] getRecordFieldNames(Class<?> recordType) {
return RecordAccessor.instance().getRecordFieldNames(recordType);
}

public static AnnotatedConstructor findRecordConstructor(DeserializationContext ctxt,
BeanDescription beanDesc, List<String> names) {
return new CreatorLocator(ctxt, beanDesc)
.locate(names);
}

static class RecordAccessor {
private final Method RECORD_GET_RECORD_COMPONENTS;
private final Method RECORD_COMPONENT_GET_NAME;
Expand Down Expand Up @@ -74,6 +94,32 @@ public String[] getRecordFieldNames(Class<?> recordType) throws IllegalArgumentE
return names;
}

public RawTypeName[] getRecordFields(Class<?> recordType) throws IllegalArgumentException
{
final Object[] components = recordComponents(recordType);
final RawTypeName[] results = new RawTypeName[components.length];
for (int i = 0; i < components.length; i++) {
String name;
try {
name = (String) RECORD_COMPONENT_GET_NAME.invoke(components[i]);
} catch (Exception e) {
throw new IllegalArgumentException(String.format(
"Failed to access name of field #%d (of %d) of Record type %s",
i, components.length, ClassUtil.nameOf(recordType)), e);
}
Class<?> type;
try {
type = (Class<?>) RECORD_COMPONENT_GET_TYPE.invoke(components[i]);
} catch (Exception e) {
throw new IllegalArgumentException(String.format(
"Failed to access type of field #%d (of %d) of Record type %s",
i, components.length, ClassUtil.nameOf(recordType)), e);
}
results[i] = new RawTypeName(type, name);
}
return results;
}

protected Object[] recordComponents(Class<?> recordType) throws IllegalArgumentException
{
try {
Expand All @@ -84,4 +130,92 @@ protected Object[] recordComponents(Class<?> recordType) throws IllegalArgumentE
}
}
}

static class RawTypeName {
public final Class<?> rawType;
public final String name;

public RawTypeName(Class<?> rt, String n) {
rawType = rt;
name = n;
}
}

static class CreatorLocator {
protected final BeanDescription _beanDesc;
protected final DeserializationConfig _config;
protected final AnnotationIntrospector _intr;

protected final List<AnnotatedConstructor> _constructors;
protected final AnnotatedConstructor _primaryConstructor;
protected final RawTypeName[] _recordFields;

CreatorLocator(DeserializationContext ctxt, BeanDescription beanDesc)
{
_beanDesc = beanDesc;

_intr = ctxt.getAnnotationIntrospector();
_config = ctxt.getConfig();

_recordFields = RecordAccessor.instance().getRecordFields(beanDesc.getBeanClass());
final int argCount = _recordFields.length;

// And then locate the canonical constructor; must be found, if not, fail
// altogether (so we can figure out what went wrong)
AnnotatedConstructor primary = null;

// One special case: empty Records, empty constructor is separate case
if (argCount == 0) {
primary = beanDesc.findDefaultConstructor();
_constructors = Collections.singletonList(primary);
} else {
_constructors = beanDesc.getConstructors();
main_loop:
for (AnnotatedConstructor ctor : _constructors) {
if (ctor.getParameterCount() != argCount) {
continue;
}
for (int i = 0; i < argCount; ++i) {
if (!ctor.getRawParameterType(i).equals(_recordFields[i].rawType)) {
continue main_loop;
}
}
primary = ctor;
break;
}
}
if (primary == null) {
throw new IllegalArgumentException("Failed to find the canonical Record constructor of type "
+ClassUtil.getTypeDescription(_beanDesc.getType()));
}
_primaryConstructor = primary;
}

public AnnotatedConstructor locate(List<String> names)
{
// First things first: ensure that either there are no explicit marked constructors
// or that there is just one and it is the canonical one and it is not
// declared as "delegating" constructor
for (AnnotatedConstructor ctor : _constructors) {
JsonCreator.Mode creatorMode = _intr.findCreatorAnnotation(_config, ctor);
if ((null == creatorMode) || (Mode.DISABLED == creatorMode)) {
continue;
}
// If there's a delegating Creator let caller figure out
if (Mode.DELEGATING == creatorMode) {
return null;
}
if (ctor != _primaryConstructor) {
return null;
}
}

// By now we have established that the canonical constructor is the one to use
// and just need to gather implicit names to return
for (RawTypeName field : _recordFields) {
names.add(field.name);
}
return _primaryConstructor;
}
}
}
27 changes: 11 additions & 16 deletions src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ public RecordWithAltCtor(@JsonProperty("id") int id) {
}
}

record JsonIgnoreRecord(int id, @JsonIgnore String name) { }
record RecordWithIgnore(int id, @JsonIgnore String name) { }

record JsonPropertyRenameRecord(int id, @JsonProperty("rename")String name) { }
record RecordWithRename(int id, @JsonProperty("rename")String name) { }

record EmptyRecord() { }

Expand All @@ -50,17 +50,17 @@ public void testClassUtil() {

assertTrue(ClassUtil.isRecordType(SimpleRecord.class));
assertTrue(ClassUtil.isRecordType(RecordOfRecord.class));
assertTrue(ClassUtil.isRecordType(JsonIgnoreRecord.class));
assertTrue(ClassUtil.isRecordType(JsonPropertyRenameRecord.class));
assertTrue(ClassUtil.isRecordType(RecordWithIgnore.class));
assertTrue(ClassUtil.isRecordType(RecordWithRename.class));
}

public void testRecordJavaType() {
assertFalse(MAPPER.constructType(getClass()).isRecordType());

assertTrue(MAPPER.constructType(SimpleRecord.class).isRecordType());
assertTrue(MAPPER.constructType(RecordOfRecord.class).isRecordType());
assertTrue(MAPPER.constructType(JsonIgnoreRecord.class).isRecordType());
assertTrue(MAPPER.constructType(JsonPropertyRenameRecord.class).isRecordType());
assertTrue(MAPPER.constructType(RecordWithIgnore.class).isRecordType());
assertTrue(MAPPER.constructType(RecordWithRename.class).isRecordType());
}

/*
Expand Down Expand Up @@ -130,10 +130,7 @@ public void testDeserializeSimpleRecord_DisableAnnotationIntrospector() throws E
*/

public void testSerializeJsonIgnoreRecord() throws Exception {
JsonIgnoreRecord record = new JsonIgnoreRecord(123, "Bob");

String json = MAPPER.writeValueAsString(record);

String json = MAPPER.writeValueAsString(new RecordWithIgnore(123, "Bob"));
assertEquals("{\"id\":123}", json);
}

Expand All @@ -152,17 +149,15 @@ public void testDeserializeWithAltCtor() throws Exception {
}

public void testSerializeJsonRenameRecord() throws Exception {
JsonPropertyRenameRecord record = new JsonPropertyRenameRecord(123, "Bob");

String json = MAPPER.writeValueAsString(record);
String json = MAPPER.writeValueAsString(new RecordWithRename(123, "Bob"));
final Object EXP = map("id", Integer.valueOf(123), "rename", "Bob");
assertEquals(EXP, MAPPER.readValue(json, Object.class));
}

public void testDeserializeJsonRenameRecord() throws Exception {
JsonPropertyRenameRecord value = MAPPER.readValue("{\"id\":123,\"rename\":\"Bob\"}",
JsonPropertyRenameRecord.class);
assertEquals(new JsonPropertyRenameRecord(123, "Bob"), value);
RecordWithRename value = MAPPER.readValue("{\"id\":123,\"rename\":\"Bob\"}",
RecordWithRename.class);
assertEquals(new RecordWithRename(123, "Bob"), value);
}

private Map<String,Object> map(String key1, Object value1,
Expand Down

0 comments on commit 263b544

Please sign in to comment.