Skip to content

Commit

Permalink
feat: add record deserialization support
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-grgt committed Feb 29, 2024
1 parent 18cdbd4 commit 123a0f4
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public void setRootInitializer(Supplier<Object> rootInitializer) {
this.rootInitializer = rootInitializer;
}

public PathWriter setValueInitializer(Consumer<Object> valueInitializer) {
this.valueInitializer = valueInitializer;
return this;
}

public static PathWriter valueInitializer(Consumer<Object> o) {
PathWriter pathWriter = new PathWriter();
pathWriter.valueInitializer = o;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,15 @@ public <T> Map<Path, PathWriter> createIndexForType(Class<T> type, String rootTa

private <T> Map<Path, PathWriter> buildIndex(Class<T> type, Path path) {
Map<Path, PathWriter> 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<T> 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<Path, PathWriter> doBuildIndex(Class<?> type,
Expand All @@ -62,13 +68,27 @@ private void indexField(FieldReflector field, Map<Path, PathWriter> 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<Path, PathWriter> index, Path path, Supplier<Object> parent) {
private void indexRecordType(FieldReflector field, Map<Path, PathWriter> index, Path path, Supplier<Object> 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<Path, PathWriter> index, Path path, Supplier<Object> parent) {
Path pathForField = getPathForField(field, path);
if (pathForField.isRoot()) {
indexMapAsRootType(field, index, parent, pathForField);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T> {
private final Map<String, Object> fieldMapping = new HashMap<>();
private final Class<T> type;

public RecordWrapper(Class<T> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public <T extends Annotation> boolean hasAnnotation(Class<T> annotation) {
return field.getAnnotation(annotation) != null;
}

public boolean isRecord() {
return field.getType().isRecord();
}

@Override
public String toString() {
return new StringJoiner(", ", FieldReflector.class.getSimpleName() + "[", "]")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ static class SlashSuffixedHolder {
}

@Test
void ignoreSufAndPrefixedWhiteSpaceInPathMappings() {
void ignoreSuffixedAndPrefixedWhiteSpaceInPathMappings() {
// given
String data = """
<?xml version="1.0" encoding="UTF-8"?>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = """
<?xml version="1.0" encoding="UTF-8"?>
<Person>
<name>John</name>
</Person>
""";

// when
Person person = new XjxSerdes().read(data, Person.class);

// then
assertThat(person.name()).isEqualTo("John");
}

@Test
void recordAsField() {
// given
String data = """
<?xml version="1.0" encoding="UTF-8"?>
<House>
<Person>
<name>John</name>
</Person>
</House>
""";

// 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 = """
<?xml version="1.0" encoding="UTF-8"?>
<House>
<Person>
<name>John</name>
</Person>
</House>
""";

// 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 = """
<?xml version="1.0" encoding="UTF-8"?>
<House>
<Person>
<name>John</name>
</Person>
</House>
""";

// when
Person person = new XjxSerdes().read(data, Person.class);

// then
assertThat(person.name()).isEqualTo("John");
}

@Test
void recordWithComplexType() {
// given
String data = """
<?xml version="1.0" encoding="UTF-8"?>
<Computer>
<Brand>
<Name>Apple</Name>
</Brand>
</Computer>
""";

// when
Computer computer = new XjxSerdes().read(data, Computer.class);

// then
assertThat(computer.brand.name).isEqualTo("Apple");
}

@Test
void recordWithListType() {
// given
String data = """
<?xml version="1.0" encoding="UTF-8"?>
<Computers>
<Brand>
<Name>Apple</Name>
</Brand>
<Brand>
<Name>Commodore</Name>
</Brand>
</Computers>
""";

// 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 = """
<?xml version="1.0" encoding="UTF-8"?>
<Person>
<name>John</name>
</Person>
""";

// 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> brand) {}

}

0 comments on commit 123a0f4

Please sign in to comment.