From 077c4d5df218f6ed1a0d852fb3baef1640e16c71 Mon Sep 17 00:00:00 2001 From: yuxin-miao Date: Fri, 4 Mar 2022 15:04:25 +0100 Subject: [PATCH] feat - record support with canonical constructor (#5) (PR #13) * tmp - record investigation / debugging * test - empty record read * test - tests showing different object decoder implementation use Co-authored-by: Maxou <5208681+mxyns@users.noreply.github.com> Co-authored-by: xinmiao <> --- .../jsoniter/ReflectionDecoderFactory.java | 2 +- .../com/jsoniter/ReflectionObjectDecoder.java | 41 ++-- .../com/jsoniter/ReflectionRecordDecoder.java | 114 ++++++++++ .../com/jsoniter/spi/ClassDescriptor.java | 17 +- src/test/java/com/jsoniter/SimpleRecord.java | 11 + src/test/java/com/jsoniter/TestRecord.java | 211 +++++++++++++++++- 6 files changed, 366 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/jsoniter/ReflectionRecordDecoder.java create mode 100644 src/test/java/com/jsoniter/SimpleRecord.java diff --git a/src/main/java/com/jsoniter/ReflectionDecoderFactory.java b/src/main/java/com/jsoniter/ReflectionDecoderFactory.java index d031cd28..2aaebc1d 100644 --- a/src/main/java/com/jsoniter/ReflectionDecoderFactory.java +++ b/src/main/java/com/jsoniter/ReflectionDecoderFactory.java @@ -24,7 +24,7 @@ public static Decoder create(ClassInfo classAndArgs) { return new ReflectionEnumDecoder(clazz); } if (clazz.isRecord()) { - return new ReflectionRecordDecoder(clazz, typeArgs); + return new ReflectionRecordDecoder(classAndArgs).create(); } return new ReflectionObjectDecoder(classAndArgs).create(); } diff --git a/src/main/java/com/jsoniter/ReflectionObjectDecoder.java b/src/main/java/com/jsoniter/ReflectionObjectDecoder.java index e1e76f73..6c2bf65b 100644 --- a/src/main/java/com/jsoniter/ReflectionObjectDecoder.java +++ b/src/main/java/com/jsoniter/ReflectionObjectDecoder.java @@ -9,20 +9,20 @@ class ReflectionObjectDecoder { - private static Object NOT_SET = new Object() { + protected static Object NOT_SET = new Object() { @Override public String toString() { return "NOT_SET"; } }; - private Map allBindings = new HashMap(); - private String tempCacheKey; - private String ctorArgsCacheKey; - private int tempCount; - private long expectedTracker; - private int requiredIdx; - private int tempIdx; - private ClassDescriptor desc; + protected Map allBindings = new HashMap(); + protected String tempCacheKey; + protected String ctorArgsCacheKey; + protected int tempCount; + protected long expectedTracker; + protected int requiredIdx; + protected int tempIdx; + protected ClassDescriptor desc; public ReflectionObjectDecoder(ClassInfo classInfo) { try { @@ -34,7 +34,9 @@ public ReflectionObjectDecoder(ClassInfo classInfo) { } } - private final void init(ClassInfo classInfo) throws Exception { + protected final void init(ClassInfo classInfo) throws Exception { + + System.out.println("INIT"); Class clazz = classInfo.clazz; ClassDescriptor desc = ClassDescriptor.getDecodingClassDescriptor(classInfo, true); for (Binding param : desc.ctor.parameters) { @@ -116,6 +118,7 @@ public class OnlyField implements Decoder { public Object decode(JsonIterator iter) throws IOException { try { + System.out.println("ONLY FIELD"); return decode_(iter); } catch (RuntimeException e) { throw e; @@ -181,6 +184,7 @@ public class WithCtor implements Decoder { @Override public Object decode(JsonIterator iter) throws IOException { try { + System.out.println("WITH CTOR"); return decode_(iter); } catch (RuntimeException e) { throw e; @@ -260,6 +264,7 @@ public class WithWrapper implements Decoder { @Override public Object decode(JsonIterator iter) throws IOException { try { + System.out.println("WITH WRAPPER"); return decode_(iter); } catch (RuntimeException e) { throw e; @@ -346,7 +351,7 @@ private void setToBinding(Object obj, Binding binding, Object value) throws Exce } } - private void setExtra(Object obj, Map extra) throws Exception { + protected void setExtra(Object obj, Map extra) throws Exception { if (extra == null) { return; } @@ -367,24 +372,24 @@ private void setExtra(Object obj, Map extra) throws Exception { } } - private boolean canNotSetDirectly(Binding binding) { + protected boolean canNotSetDirectly(Binding binding) { return binding.field == null && binding.method == null; } - private Object decodeBinding(JsonIterator iter, Binding binding) throws Exception { + protected Object decodeBinding(JsonIterator iter, Binding binding) throws Exception { Object value; value = binding.decoder.decode(iter); return value; } - private Object decodeBinding(JsonIterator iter, Object obj, Binding binding) throws Exception { + protected Object decodeBinding(JsonIterator iter, Object obj, Binding binding) throws Exception { if (binding.valueCanReuse) { CodegenAccess.setExistingObject(iter, binding.field.get(obj)); } return decodeBinding(iter, binding); } - private Map onUnknownProperty(JsonIterator iter, Slice fieldName, Map extra) throws IOException { + protected Map onUnknownProperty(JsonIterator iter, Slice fieldName, Map extra) throws IOException { boolean shouldReadValue = desc.asExtraForUnknownProperties || !desc.keyValueTypeWrappers.isEmpty(); if (shouldReadValue) { Any value = iter.readAny(); @@ -398,7 +403,7 @@ private Map onUnknownProperty(JsonIterator iter, Slice fieldName return extra; } - private List collectMissingFields(long tracker) { + protected List collectMissingFields(long tracker) { List missingFields = new ArrayList(); for (Binding binding : allBindings.values()) { if (binding.asMissingWhenNotPresent) { @@ -409,7 +414,7 @@ private List collectMissingFields(long tracker) { return missingFields; } - private void applyWrappers(Object[] temp, Object obj) throws Exception { + protected void applyWrappers(Object[] temp, Object obj) throws Exception { for (WrapperDescriptor wrapper : desc.bindingTypeWrappers) { Object[] args = new Object[wrapper.parameters.size()]; for (int i = 0; i < wrapper.parameters.size(); i++) { @@ -422,7 +427,7 @@ private void applyWrappers(Object[] temp, Object obj) throws Exception { } } - private Object createNewObject(JsonIterator iter, Object[] temp) throws Exception { + protected Object createNewObject(JsonIterator iter, Object[] temp) throws Exception { if (iter.tempObjects == null) { iter.tempObjects = new HashMap(); } diff --git a/src/main/java/com/jsoniter/ReflectionRecordDecoder.java b/src/main/java/com/jsoniter/ReflectionRecordDecoder.java new file mode 100644 index 00000000..3b250ae4 --- /dev/null +++ b/src/main/java/com/jsoniter/ReflectionRecordDecoder.java @@ -0,0 +1,114 @@ +package com.jsoniter; + +import com.jsoniter.spi.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class ReflectionRecordDecoder extends ReflectionObjectDecoder { + + private boolean useOnlyFieldRecord = false; + + public ReflectionRecordDecoder(ClassInfo classInfo) { + + super(classInfo); + + if (desc.clazz.isRecord() && !desc.fields.isEmpty() && tempCount == 0) { + tempCount = tempIdx; + tempCacheKey = "temp@" + desc.clazz.getName(); + ctorArgsCacheKey = "ctor@" + desc.clazz.getName(); + + desc.ctor.parameters.addAll(desc.fields); + useOnlyFieldRecord = true; + } + } + + @Override + public Decoder create() { + + if (useOnlyFieldRecord) + return new OnlyFieldRecord(); + + if (desc.ctor.parameters.isEmpty()) { + if (desc.bindingTypeWrappers.isEmpty()) { + return new OnlyFieldRecord(); + } else { + return new WithWrapper(); + } + } else { + return new WithCtor(); + } + } + + public class OnlyFieldRecord implements Decoder { + + @Override + public Object decode(JsonIterator iter) throws IOException { + + try { + System.out.println("ONLY FIELD RECORD"); + return decode_(iter); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new JsonException(e); + } + } + + private Object decode_(JsonIterator iter) throws Exception { + if (iter.readNull()) { + CodegenAccess.resetExistingObject(iter); + return null; + } + if (iter.tempObjects == null) { + iter.tempObjects = new HashMap(); + } + Object[] temp = (Object[]) iter.tempObjects.get(tempCacheKey); + if (temp == null) { + temp = new Object[tempCount]; + iter.tempObjects.put(tempCacheKey, temp); + } + Arrays.fill(temp, NOT_SET); + if (!CodegenAccess.readObjectStart(iter)) { + if (requiredIdx > 0) { + throw new JsonException("missing required properties: " + collectMissingFields(0)); + } + return createNewObject(iter, temp); + } + Map extra = null; + long tracker = 0L; + Slice fieldName = CodegenAccess.readObjectFieldAsSlice(iter); + Binding binding = allBindings.get(fieldName); + if (binding == null) { + extra = onUnknownProperty(iter, fieldName, extra); + } else { + if (binding.asMissingWhenNotPresent) { + tracker |= binding.mask; + } + temp[binding.idx] = decodeBinding(iter, binding); + } + while (CodegenAccess.nextToken(iter) == ',') { + fieldName = CodegenAccess.readObjectFieldAsSlice(iter); + binding = allBindings.get(fieldName); + if (binding == null) { + extra = onUnknownProperty(iter, fieldName, extra); + } else { + if (binding.asMissingWhenNotPresent) { + tracker |= binding.mask; + } + temp[binding.idx] = decodeBinding(iter, binding); + } + } + if (tracker != expectedTracker) { + throw new JsonException("missing required properties: " + collectMissingFields(tracker)); + } + Object obj = createNewObject(iter, temp.clone()); + setExtra(obj, extra); + applyWrappers(temp, obj); + return obj; + } + + } +} diff --git a/src/main/java/com/jsoniter/spi/ClassDescriptor.java b/src/main/java/com/jsoniter/spi/ClassDescriptor.java index a47dbe5d..98217398 100644 --- a/src/main/java/com/jsoniter/spi/ClassDescriptor.java +++ b/src/main/java/com/jsoniter/spi/ClassDescriptor.java @@ -31,7 +31,7 @@ public static ClassDescriptor getDecodingClassDescriptor(ClassInfo classInfo, bo desc.classInfo = classInfo; desc.clazz = clazz; desc.lookup = lookup; - desc.ctor = getCtor(clazz); + desc.ctor = clazz.isRecord() ? getRecordCtor(clazz) : getCtor(clazz); desc.setters = getSetters(lookup, classInfo, includingPrivate); desc.getters = new ArrayList(); desc.fields = getFields(lookup, classInfo, includingPrivate); @@ -203,6 +203,20 @@ private static ConstructorDescriptor getCtor(Class clazz) { return cctor; } + private static ConstructorDescriptor getRecordCtor(Class clazz) { + ConstructorDescriptor cctor = new ConstructorDescriptor(); + try { + Constructor ctor = clazz.getDeclaredConstructors()[0]; + cctor.ctor = ctor; + for (Type parameter : ctor.getParameterTypes()) { + ClassInfo info = new ClassInfo(parameter); + } + } catch (Exception e) { + cctor.ctor = null; + } + return cctor; + } + private static List getFields(Map lookup, ClassInfo classInfo, boolean includingPrivate) { ArrayList bindings = new ArrayList(); for (Field field : getAllFields(classInfo.clazz)) { @@ -432,7 +446,6 @@ public List allDecoderBindings() { return bindings; } - public List allEncoderBindings() { ArrayList bindings = new ArrayList(8); bindings.addAll(fields); diff --git a/src/test/java/com/jsoniter/SimpleRecord.java b/src/test/java/com/jsoniter/SimpleRecord.java new file mode 100644 index 00000000..7a740468 --- /dev/null +++ b/src/test/java/com/jsoniter/SimpleRecord.java @@ -0,0 +1,11 @@ +package com.jsoniter; + +public record SimpleRecord(String field1, String field2) { + public SimpleRecord() { + this(null, null); + } + public SimpleRecord(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } +} \ No newline at end of file diff --git a/src/test/java/com/jsoniter/TestRecord.java b/src/test/java/com/jsoniter/TestRecord.java index ddf50b96..1dbcc138 100644 --- a/src/test/java/com/jsoniter/TestRecord.java +++ b/src/test/java/com/jsoniter/TestRecord.java @@ -1,31 +1,224 @@ package com.jsoniter; -import com.jsoniter.output.JsonStream; + +import com.jsoniter.annotation.JsonCreator; +import com.jsoniter.annotation.JsonProperty; +import com.jsoniter.any.Any; +import com.jsoniter.spi.ClassInfo; +import com.jsoniter.spi.EmptyExtension; import com.jsoniter.spi.JsonException; -import junit.framework.Test; +import com.jsoniter.spi.JsoniterSpi; import junit.framework.TestCase; import java.io.IOException; -import java.util.Arrays; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Map; public class TestRecord extends TestCase { - record TestRecord1(long field1) { + record TestRecord1(long field1) {} + + public record TestRecord0(Long id, String name) { + + public TestRecord0() { + + this(0L, ""); + } + } + + public void test_print_record_reflection_info() { + + Class clazz = TestRecord1.class; + + System.out.println("Record Constructors :"); + for (Constructor constructor : clazz.getConstructors()) { + System.out.println(constructor); + } + + System.out.println("Record Methods : "); + for (Method method : clazz.getMethods()) { + System.out.println(method); + } + + System.out.println("Record Fields : "); + for (Field field : clazz.getFields()) { + System.out.println(field); + System.out.println(" modifiers : " + Modifier.toString(field.getModifiers())); + } + + System.out.println("Record Declared Fields : "); + for (Field field : clazz.getDeclaredFields()) { + System.out.println(field); + System.out.println(" modifiers : " + Modifier.toString(field.getModifiers())); + } + + try { + System.out.println("Record Default Declared Constructor : " + clazz.getDeclaredConstructor()); + } catch (Exception ex) { + System.err.println("No Record Default Declared Constructor!"); + } + + System.out.println("Record Declared Constructors : "); + for (Constructor constructor : clazz.getDeclaredConstructors()) { + System.out.println(constructor); + System.out.println(" name : " + constructor.getName()); + System.out.println(" modifiers : " + Modifier.toString(constructor.getModifiers())); + System.out.println(" input count : " + constructor.getParameterCount()); + System.out.println(" input types : "); + for (Class parameter : constructor.getParameterTypes()) + System.out.println(" " + parameter); + } + } + + public void test_empty_record() throws IOException { + + JsonIterator iter = JsonIterator.parse("{}"); + assertNotNull(iter.read(TestRecord0.class)); + } + public void test_empty_simple_record() throws IOException { + + JsonIterator iter = JsonIterator.parse("{}"); + SimpleRecord simpleRecord = iter.read(SimpleRecord.class); + assertNull(simpleRecord.field1()); + iter.reset(iter.buf); + Object obj = iter.read(Object.class); + assertEquals(0, ((Map) obj).size()); + iter.reset(iter.buf); + Any any = iter.readAny(); + assertEquals(0, any.size()); } public void test_record_error() throws IOException { - JsonIterator iter = JsonIterator.parse("{ 'field1' : 1".replace('\'', '"')); - try{ + JsonIterator iter = JsonIterator.parse("{ 'field1' : 1 }".replace('\'', '"')); + try { TestRecord1 rec = iter.read(TestRecord1.class); assertEquals(1, rec.field1); - }catch (JsonException e) { + } catch (JsonException e) { throw new JsonException("no constructor for: class com.jsoniter.TestRecord", e); } } - public void test_record_serialize(){ - assertEquals("{\"field1\":1}",JsonStream.serialize(new TestRecord1(1))); + + public void test_record_withOnlyFieldDecoder() throws IOException { + + assertEquals(ReflectionRecordDecoder.OnlyFieldRecord.class, ReflectionDecoderFactory.create(new ClassInfo(TestRecord1.class)).getClass()); + + JsonIterator iter = JsonIterator.parse("{ 'field1' : 1 }".replace('\'', '"')); + TestRecord1 record = iter.read(TestRecord1.class); + + assertEquals(1, record.field1); + } + + public void test_record_2_fields_withOnlyFieldDecoder() throws IOException { + + record TestRecord2(long field1, String field2) {} + + assertEquals(ReflectionRecordDecoder.OnlyFieldRecord.class, ReflectionDecoderFactory.create(new ClassInfo(TestRecord2.class)).getClass()); + + JsonIterator iter = JsonIterator.parse("{ 'field1' : 1, 'field2' : 'hey' }".replace('\'', '"')); + TestRecord2 record = iter.read(TestRecord2.class); + + assertEquals(1, record.field1); + assertEquals("hey", record.field2); + } + + public void test_record_2_fields_swapFieldOrder_withOnlyFieldDecoder() throws IOException { + + record TestRecord2(String field2, long field1) {} + + assertEquals(ReflectionRecordDecoder.OnlyFieldRecord.class, ReflectionDecoderFactory.create(new ClassInfo(TestRecord2.class)).getClass()); + + JsonIterator iter = JsonIterator.parse("{ 'field2' : 'hey', 'field1' : 1 }".replace('\'', '"')); + TestRecord2 record = iter.read(TestRecord2.class); + + assertEquals(1, record.field1); + assertEquals("hey", record.field2); + } + + public void test_record_recordComposition_withOnlyFieldDecoder() throws IOException { + + record TestRecordA(long fieldA) {} + record TestRecordB(long fieldB, TestRecordA a) {} + + assertEquals(ReflectionRecordDecoder.OnlyFieldRecord.class, ReflectionDecoderFactory.create(new ClassInfo(TestRecordB.class)).getClass()); + + JsonIterator iter = JsonIterator.parse("{ 'fieldB' : 1, 'a' : { 'fieldA' : 69 } }".replace('\'', '"')); + TestRecordB record = iter.read(TestRecordB.class); + + assertEquals(1, record.fieldB); + assertEquals(69, record.a.fieldA); + } + + public void test_record_empty_constructor_withOnlyFieldDecoder() throws IOException { + + record TestRecord3() {} + + assertEquals(ReflectionRecordDecoder.OnlyFieldRecord.class, ReflectionDecoderFactory.create(new ClassInfo(TestRecord3.class)).getClass()); + + JsonIterator iter = JsonIterator.parse("{ 'fieldB' : 1, 'a' : { 'fieldA' : 69 } }".replace('\'', '"')); + TestRecord3 record = iter.read(TestRecord3.class); + + assertNotNull(record); + } + + public void test_enum() throws IOException { + + record TestRecord5(MyEnum field1) { + + enum MyEnum { + HELLO, + WOW + } + } + + TestRecord5 obj = JsonIterator.deserialize("{\"field1\":\"HELLO\"}", TestRecord5.class); + assertEquals(TestRecord5.MyEnum.HELLO, obj.field1); + try { + JsonIterator.deserialize("{\"field1\":\"HELLO1\"}", TestRecord5.class); + fail(); + } catch (JsonException e) { + } + obj = JsonIterator.deserialize("{\"field1\":null}", TestRecord5.class); + assertNull(obj.field1); + obj = JsonIterator.deserialize("{\"field1\":\"WOW\"}", TestRecord5.class); + assertEquals(TestRecord5.MyEnum.WOW, obj.field1); + } + + public void test_record_2_constructors_withOnlyFieldDecoder() throws IOException { + + record TestRecord6(long val) { + + public TestRecord6(int valInt) { + this(Long.valueOf(valInt)); + } + } + + assertEquals(ReflectionRecordDecoder.OnlyFieldRecord.class, ReflectionDecoderFactory.create(new ClassInfo(TestRecord6.class)).getClass()); + + JsonIterator iter = JsonIterator.parse("{ 'valInt' : 1 }".replace('\'', '"')); + TestRecord6 record = iter.read(TestRecord6.class); + + assertNotNull(record); + } + + public void test_record_withCtorDecoder() throws IOException { + + record TestRecord2(@JsonProperty long field1) { + + @JsonCreator + TestRecord2 {} + } + + assertEquals(ReflectionDecoderFactory.create(new ClassInfo(TestRecord2.class)).getClass(), ReflectionObjectDecoder.WithCtor.class); + + JsonIterator iter = JsonIterator.parse("{ 'field1' : 1 }".replace('\'', '"')); + TestRecord2 record = iter.read(TestRecord2.class); + + assertEquals(1, record.field1); } }