From 123a0f4b485f9407c91aabc1045a4e6321155b5e Mon Sep 17 00:00:00 2001 From: JonasG Date: Thu, 29 Feb 2024 16:57:45 +0100 Subject: [PATCH] feat: add record deserialization support --- .../deserialize/PathBasedSaxHandler.java | 9 +- .../xjx/serdes/deserialize/PathWriter.java | 5 + .../deserialize/PathWriterIndexFactory.java | 30 ++- .../xjx/serdes/deserialize/RecordWrapper.java | 40 ++++ .../deserialize/accessor/FieldAccessor.java | 17 +- .../accessor/RecordFieldAccessor.java | 21 +++ .../xjx/serdes/reflector/FieldReflector.java | 4 + .../GeneralDeserializationTest.java | 2 +- .../JavaRecordDeserializationTest.java | 175 ++++++++++++++++++ 9 files changed, 290 insertions(+), 13 deletions(-) create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/RecordWrapper.java create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/RecordFieldAccessor.java create mode 100644 xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/JavaRecordDeserializationTest.java diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathBasedSaxHandler.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathBasedSaxHandler.java index e8627bc..2deff16 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathBasedSaxHandler.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathBasedSaxHandler.java @@ -90,6 +90,9 @@ public void endTag(String namespace, String name) { pathWriter.getValueInitializer().accept(data); } if (pathWriter.getObjectInitializer() != null && !objectInstances.isEmpty() && objectInstances.size() != 1) { + if (pathWriter.getValueInitializer() != null) { + pathWriter.getValueInitializer().accept(objectInstances.peek()); + } objectInstances.pop(); } } @@ -123,6 +126,10 @@ private void handleRootTag(String name) { @SuppressWarnings("unchecked") public T instance() { - return (T) objectInstances.pop(); + Object instance = objectInstances.pop(); + if (instance instanceof RecordWrapper recordWrapper) { + return (T) recordWrapper.record(); + } + return (T) instance; } } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriter.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriter.java index a17e69f..33d59d4 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriter.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriter.java @@ -27,6 +27,11 @@ public void setRootInitializer(Supplier rootInitializer) { this.rootInitializer = rootInitializer; } + public PathWriter setValueInitializer(Consumer valueInitializer) { + this.valueInitializer = valueInitializer; + return this; + } + public static PathWriter valueInitializer(Consumer o) { PathWriter pathWriter = new PathWriter(); pathWriter.valueInitializer = o; diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndexFactory.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndexFactory.java index 24b9383..6dd1720 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndexFactory.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndexFactory.java @@ -38,9 +38,15 @@ public Map createIndexForType(Class type, String rootTa private Map buildIndex(Class type, Path path) { Map index = new HashMap<>(); - T root = TypeReflector.reflect(type).instanceReflector().instance(); - index.put(path, PathWriter.rootInitializer(() -> root)); - return doBuildIndex(type, path, index, () -> root); + if (type.isRecord()) { + RecordWrapper recordWrapper = new RecordWrapper<>(type); + index.put(path, PathWriter.rootInitializer(() -> recordWrapper)); + return doBuildIndex(type, path, index, () -> recordWrapper); + } else { + T root = TypeReflector.reflect(type).instanceReflector().instance(); + index.put(path, PathWriter.rootInitializer(() -> root)); + return doBuildIndex(type, path, index, () -> root); + } } private Map doBuildIndex(Class type, @@ -62,13 +68,27 @@ private void indexField(FieldReflector field, Map index, Path } else if (Map.class.equals(field.type())) { indexMapType(field, index, path, parent); } else if (field.type().isEnum()) { - indexEnumType(field, index, path, parent); + indexEnumType(field, index, path, parent); + } else if (field.isRecord()) { + indexRecordType(field, index, path, parent); } else { indexComplexType(field, index, path, parent); } } - private void indexMapType(FieldReflector field, Map index, Path path, Supplier parent) { + private void indexRecordType(FieldReflector field, Map index, Path path, Supplier parent) { + RecordWrapper recordWrapper = new RecordWrapper<>(field.type()); + index.put(getPathForField(field, path), PathWriter.objectInitializer(() -> { + return recordWrapper; + }).setValueInitializer((value) -> { + if (value instanceof RecordWrapper recordWrapperValue) { + FieldAccessor.of(field, parent.get()).set(recordWrapperValue.record()); + } + })); + doBuildIndex(field.type(), getPathForField(field, path), index, () -> recordWrapper); + } + + private void indexMapType(FieldReflector field, Map index, Path path, Supplier parent) { Path pathForField = getPathForField(field, path); if (pathForField.isRoot()) { indexMapAsRootType(field, index, parent, pathForField); diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/RecordWrapper.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/RecordWrapper.java new file mode 100644 index 0000000..a63c5bf --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/RecordWrapper.java @@ -0,0 +1,40 @@ +package io.jonasg.xjx.serdes.deserialize; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +public class RecordWrapper { + private final Map fieldMapping = new HashMap<>(); + private final Class type; + + public RecordWrapper(Class type) { + this.type = type; + } + + public void set(String name, Object value) { + this.fieldMapping.put(name, value); + } + + public T record() { + try { + Constructor[] constructors = type.getDeclaredConstructors(); + + Constructor constructor = constructors[0]; + constructor.setAccessible(true); + + Object[] args = new Object[constructor.getParameterCount()]; + var parameters = constructor.getParameters(); + for (int i = 0; i < parameters.length; i++) { + String paramName = parameters[i].getName(); + args[i] = fieldMapping.getOrDefault(paramName, null); + } + + return (T) constructor.newInstance(args); + + } catch (Exception e) { + throw new RuntimeException("Error creating record", e); + } + } +} diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/FieldAccessor.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/FieldAccessor.java index aba5e68..ee35ba8 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/FieldAccessor.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/FieldAccessor.java @@ -1,17 +1,22 @@ package io.jonasg.xjx.serdes.deserialize.accessor; import io.jonasg.xjx.serdes.TypeMappers; +import io.jonasg.xjx.serdes.deserialize.RecordWrapper; import io.jonasg.xjx.serdes.reflector.FieldReflector; public interface FieldAccessor { static FieldAccessor of(FieldReflector field, Object instance) { - var setterFieldAccessor = new SetterFieldAccessor(field, instance); - if (setterFieldAccessor.hasSetterForField()) { - return new SetterFieldAccessor(field, instance); - } - var mapper = TypeMappers.forType(field.type()); - return new ReflectiveFieldAccessor(field, instance, mapper); + if (instance instanceof RecordWrapper recordWrapper) { + return new RecordFieldAccessor(field, recordWrapper); + } else { + var setterFieldAccessor = new SetterFieldAccessor(field, instance); + if (setterFieldAccessor.hasSetterForField()) { + return new SetterFieldAccessor(field, instance); + } + var mapper = TypeMappers.forType(field.type()); + return new ReflectiveFieldAccessor(field, instance, mapper); + } } void set(Object value); diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/RecordFieldAccessor.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/RecordFieldAccessor.java new file mode 100644 index 0000000..4912529 --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/RecordFieldAccessor.java @@ -0,0 +1,21 @@ +package io.jonasg.xjx.serdes.deserialize.accessor; + +import io.jonasg.xjx.serdes.deserialize.RecordWrapper; +import io.jonasg.xjx.serdes.reflector.FieldReflector; + +public class RecordFieldAccessor implements FieldAccessor { + + private final FieldReflector field; + + private final RecordWrapper recordWrapper; + + public RecordFieldAccessor(FieldReflector field, RecordWrapper recordWrapper) { + this.field = field; + this.recordWrapper = recordWrapper; + } + + @Override + public void set(Object value) { + recordWrapper.set(field.rawField().getName(), value); + } +} diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/FieldReflector.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/FieldReflector.java index c1542a3..1d6f1a5 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/FieldReflector.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/FieldReflector.java @@ -51,6 +51,10 @@ public boolean hasAnnotation(Class annotation) { return field.getAnnotation(annotation) != null; } + public boolean isRecord() { + return field.getType().isRecord(); + } + @Override public String toString() { return new StringJoiner(", ", FieldReflector.class.getSimpleName() + "[", "]") diff --git a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralDeserializationTest.java b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralDeserializationTest.java index 955e607..e260233 100644 --- a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralDeserializationTest.java +++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralDeserializationTest.java @@ -196,7 +196,7 @@ static class SlashSuffixedHolder { } @Test - void ignoreSufAndPrefixedWhiteSpaceInPathMappings() { + void ignoreSuffixedAndPrefixedWhiteSpaceInPathMappings() { // given String data = """ diff --git a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/JavaRecordDeserializationTest.java b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/JavaRecordDeserializationTest.java new file mode 100644 index 0000000..2b67f4c --- /dev/null +++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/JavaRecordDeserializationTest.java @@ -0,0 +1,175 @@ +package io.jonasg.xjx.serdes.deserialize; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.jonasg.xjx.serdes.Tag; +import io.jonasg.xjx.serdes.XjxSerdes; + +import java.util.List; + +public class JavaRecordDeserializationTest { + + @Test + void deserializeTopLevelRecord() { + // given + record Person(@Tag(path = "/Person/name") String name) {} + String data = """ + + + John + + """; + + // when + Person person = new XjxSerdes().read(data, Person.class); + + // then + assertThat(person.name()).isEqualTo("John"); + } + + @Test + void recordAsField() { + // given + String data = """ + + + + John + + + """; + + // when + House house = new XjxSerdes().read(data, House.class); + + // then + assertThat(house.person.name()).isEqualTo("John"); + } + + @Test + void relativeMappedFieldWithWithTopLevelMappedRootType() { + // given + @Tag(path = "/House") + record Person(@Tag(path = "Person/name") String name) {} + String data = """ + + + + John + + + """; + + // when + Person person = new XjxSerdes().read(data, Person.class); + + // then + assertThat(person.name()).isEqualTo("John"); + } + + + @Test + void absoluteMappedFieldWithWithTopLevelMappedRootType() { + // given + @Tag(path = "/House") + record Person(@Tag(path = "/House/Person/name") String name) {} + String data = """ + + + + John + + + """; + + // when + Person person = new XjxSerdes().read(data, Person.class); + + // then + assertThat(person.name()).isEqualTo("John"); + } + + @Test + void recordWithComplexType() { + // given + String data = """ + + + + Apple + + + """; + + // when + Computer computer = new XjxSerdes().read(data, Computer.class); + + // then + assertThat(computer.brand.name).isEqualTo("Apple"); + } + + @Test + void recordWithListType() { + // given + String data = """ + + + + Apple + + + Commodore + + + """; + + // when + Computers computers = new XjxSerdes().read(data, Computers.class); + + // then + assertThat(computers.brand).hasSize(2); + } + + @Test + void setFieldsToNullWhenNoMappingIsFound() { + // given + record Person(@Tag(path = "/Person/name") String name, String lastName) {} + String data = """ + + + John + + """; + + // when + Person person = new XjxSerdes().read(data, Person.class); + + // then + assertThat(person.lastName()).isNull(); + } + + record Person(@Tag(path = "name") String name, String lastName) {} + + static class House { + @Tag(path = "/House/Person") + Person person; + + public House() { + } + } + + record Computer(@Tag(path = "/Computer/Brand") Brand brand) {} + + static class Brand { + @Tag(path = "Name") + String name; + + public Brand() { + } + } + + record Computers(@Tag(path = "/Computers", items = "Brand") List brand) {} + +}