Skip to content

Commit

Permalink
fix(xjx-serdes): Deserialize root tag containing repeated elements to…
Browse files Browse the repository at this point in the history
… collection types
  • Loading branch information
jonas-grgt committed Jan 10, 2024
1 parent 50ff43c commit 842f65b
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 40 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
**big.xml
**gpx.xml
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Xjx
XML serializing and deserializing (serdes) library: No Dependencies, Just Simplicity
# 🙅 Xjx
Java - XML serializing and deserializing (serdes) library: No Dependencies, Just Simplicity

# 🤔 Why
The "why" behind Xjx is rooted in the necessity for a minimalist, actively maintained XML-to-Java and vice versa library.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public static PathWriter objectInitializer(Supplier<Object> objectInitializer) {
return pathWriter;
}

public void setRootInitializer(Supplier<Object> rootInitializer) {
this.rootInitializer = rootInitializer;
}

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 @@ -175,26 +175,34 @@ private void indexSimpleType(FieldReflector field, Map<Path, PathWriter> index,
}
}

private void indexSetType(FieldReflector field, Map<Path, PathWriter> index, Path path, Supplier<Object> parent) {
private void indexSetType(FieldReflector field, Map<Path, PathWriter> index, Path parentPath, Supplier<Object> parent) {
Collection<Object> set = new HashSet<>();
index.put(getPathForField(field, path), PathWriter.objectInitializer(() -> {
Path path = getPathForField(field, parentPath);
var pathWriter = PathWriter.objectInitializer(() -> {
FieldAccessor.of(field, parent.get()).set(set);
return set;
}));
});
if (parentPath.isRoot()) {
pathWriter.setRootInitializer(() -> {
FieldAccessor.of(field, parent.get()).set(set);
return parent.get();
});
}
index.put(path, pathWriter);
Type actualTypeArgument = ((ParameterizedType) field.genericType()).getActualTypeArguments()[0];
Class<?> type = (Class<?>) actualTypeArgument;
var tag = Reflector.reflect(type).annotation(Tag.class);
Class<?> typeArgument = (Class<?>) actualTypeArgument;
var tag = Reflector.reflect(typeArgument).annotation(Tag.class);
if (tag != null) {
Supplier<Object> listTypeInstanceSupplier = collectionSupplierForType(type);
Supplier<Object> listTypeInstanceSupplier = collectionSupplierForType(typeArgument);
index.put(Path.parse(tag.path()), PathWriter.objectInitializer(() -> {
collectionCacheType.clear();
Object listTypeInstance = listTypeInstanceSupplier.get();
set.add(listTypeInstance);
return listTypeInstance;
}));
doBuildIndex(type, path, index, listTypeInstanceSupplier);
doBuildIndex(typeArgument, Path.parse(tag.path()), index, listTypeInstanceSupplier);
} else {
throw new XjxDeserializationException("Generics of type Set require @Tag pointing to mapped XML path (" + type.getSimpleName() + ")");
throw new XjxDeserializationException("Generics of type Set require @Tag pointing to mapped XML path (" + typeArgument.getSimpleName() + ")");
}
}

Expand All @@ -211,10 +219,18 @@ private Supplier<Object> collectionSupplierForType(Class<?> typeArgument) {

private void indexListType(FieldReflector field, Map<Path, PathWriter> index, Path parentPath, Supplier<Object> parent) {
List<Object> list = new ArrayList<>();
index.put(getPathForField(field, parentPath), PathWriter.objectInitializer(() -> {
Path path = getPathForField(field, parentPath);
var pathWriter = PathWriter.objectInitializer(() -> {
FieldAccessor.of(field, parent.get()).set(list);
return list;
}));
});
if (path.isRoot()) {
pathWriter.setRootInitializer(() -> {
FieldAccessor.of(field, parent.get()).set(list);
return parent.get();
});
}
index.put(path, pathWriter);
Type actualTypeArgument = ((ParameterizedType) field.genericType()).getActualTypeArguments()[0];
Class<?> typeArgument = (Class<?>) actualTypeArgument;
var tag = Reflector.reflect(typeArgument).annotation(Tag.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import java.util.List;

import static org.assertj.core.api.Assertions.*;

public class ListDeserializationTest {

@Test
Expand Down Expand Up @@ -47,9 +49,9 @@ void deserializeIntoListField_OfComplexType_ContainingTopLevelMapping() {
WeatherData weatherData = new XjxSerdes().read(data, WeatherData.class);

// then
Assertions.assertThat(weatherData.forecasts).hasSize(2);
Assertions.assertThat(weatherData.forecasts.get(0).maxTemperature).isEqualTo("71");
Assertions.assertThat(weatherData.forecasts.get(1).maxTemperature).isEqualTo("78");
assertThat(weatherData.forecasts).hasSize(2);
assertThat(weatherData.forecasts.get(0).maxTemperature).isEqualTo("71");
assertThat(weatherData.forecasts.get(1).maxTemperature).isEqualTo("78");
}

public static class WeatherData {
Expand Down Expand Up @@ -114,9 +116,9 @@ void deserializeIntoListField_OfComplexType_ContainingComplexTypesWithCustomMapp
PrecipitationData precipitationData = new XjxSerdes().read(data, PrecipitationData.class);

// then
Assertions.assertThat(precipitationData.precipitations).hasSize(2);
Assertions.assertThat(precipitationData.precipitations.get(0).precipitationValue.value).isEqualTo("10");
Assertions.assertThat(precipitationData.precipitations.get(1).precipitationValue.value).isEqualTo("12");
assertThat(precipitationData.precipitations).hasSize(2);
assertThat(precipitationData.precipitations.get(0).precipitationValue.value).isEqualTo("10");
assertThat(precipitationData.precipitations.get(1).precipitationValue.value).isEqualTo("12");
}

@Test
Expand Down Expand Up @@ -156,7 +158,7 @@ void deserializeIntoListField_EvenIfNoneOfTheInnerMappedFieldsOfComplexTypeCanBe
PrecipitationData precipitationData = new XjxSerdes().read(data, PrecipitationData.class);

// then
Assertions.assertThat(precipitationData.precipitations).hasSize(2);
assertThat(precipitationData.precipitations).hasSize(2);
}

@Test
Expand All @@ -173,7 +175,7 @@ void listsMappedOntoSelfClosingTag_containsEmptyList() {
PrecipitationData precipitationData = new XjxSerdes().read(data, PrecipitationData.class);

// then
Assertions.assertThat(precipitationData.precipitations).isEmpty();
assertThat(precipitationData.precipitations).isEmpty();
}

static class PrecipitationData {
Expand Down Expand Up @@ -242,7 +244,7 @@ void informUserThatAList_itsGenericType_shouldBeAnnotatedWithTag() {
ThrowableAssert.ThrowingCallable when = () -> new XjxSerdes().read(data, WeatherDataWithMissingTag.class);

// then
Assertions.assertThatThrownBy(when)
assertThatThrownBy(when)
.hasMessage("Generics of type List require @Tag pointing to mapped XML path (ForecastWithMissingTag)");
}

Expand Down Expand Up @@ -299,9 +301,9 @@ void deserializeIntoListField_OfComplexType_ContainingRelativeMappedField() {
var weatherData = new XjxSerdes().read(data, WeatherDataRelativeMapping.class);

// then
Assertions.assertThat(weatherData.forecasts).hasSize(2);
Assertions.assertThat(weatherData.forecasts.get(0).maxTemperature).isEqualTo("71");
Assertions.assertThat(weatherData.forecasts.get(1).maxTemperature).isEqualTo("78");
assertThat(weatherData.forecasts).hasSize(2);
assertThat(weatherData.forecasts.get(0).maxTemperature).isEqualTo("71");
assertThat(weatherData.forecasts.get(1).maxTemperature).isEqualTo("78");
}

public static class WeatherDataRelativeMapping {
Expand Down Expand Up @@ -358,11 +360,11 @@ void deserializeIntoListField_OfComplexType_ContainingRelativeAndAbsoluteMappedF
var weatherData = new XjxSerdes().read(data, WeatherDataRelativeAndAbsoluteMapping.class);

// then
Assertions.assertThat(weatherData.forecasts).hasSize(2);
Assertions.assertThat(weatherData.forecasts.get(0).maxTemperature).isEqualTo("71");
Assertions.assertThat(weatherData.forecasts.get(0).minTemperature).isEqualTo("60");
Assertions.assertThat(weatherData.forecasts.get(1).maxTemperature).isEqualTo("78");
Assertions.assertThat(weatherData.forecasts.get(1).minTemperature).isEqualTo("62");
assertThat(weatherData.forecasts).hasSize(2);
assertThat(weatherData.forecasts.get(0).maxTemperature).isEqualTo("71");
assertThat(weatherData.forecasts.get(0).minTemperature).isEqualTo("60");
assertThat(weatherData.forecasts.get(1).maxTemperature).isEqualTo("78");
assertThat(weatherData.forecasts.get(1).minTemperature).isEqualTo("62");
}

public static class WeatherDataRelativeAndAbsoluteMapping {
Expand All @@ -384,4 +386,63 @@ public ForecastRelativeAndAbsoluteMapping() {
@Tag(path = "/WeatherData/Forecasts/Day/Low/Value")
String minTemperature;
}

@Test
void deserializeIntoListField_whereRootTagContainsRepeatedElements() {
// given
String xmlDoc = """
<gpx
version="1.0"
creator="ExpertGPS 1.1 - https://www.topografix.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.topografix.com/GPX/1/0"
xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd">
<wpt lat="42.438878" lon="-71.119277">
<ele>44.586548</ele>
<time>2001-11-28T21:05:28Z</time>
<name>5066</name>
<desc><![CDATA[5066]]></desc>
<sym>Crossing</sym>
<type><![CDATA[Crossing]]></type>
</wpt>
<wpt lat="42.439227" lon="-71.119689">
<ele>57.607200</ele>
<time>2001-06-02T03:26:55Z</time>
<name>5067</name>
<desc><![CDATA[5067]]></desc>
<sym>Dot</sym>
<type><![CDATA[Intersection]]></type>
</wpt>
</gpx>""";

// when
var gpx = new XjxSerdes().read(xmlDoc, Gpx.class);

// then
assertThat(gpx.wayPoints).hasSize(2);
assertThat(gpx.wayPoints.get(0).description).isEqualTo("5066");
assertThat(gpx.wayPoints.get(0).time).isEqualTo("2001-11-28T21:05:28Z");
assertThat(gpx.wayPoints.get(1).description).isEqualTo("5067");
assertThat(gpx.wayPoints.get(1).time).isEqualTo("2001-06-02T03:26:55Z");
}

static class Gpx {
public Gpx() {
}

@Tag(path = "/gpx")
List<Wpt> wayPoints;
}

@Tag(path = "/gpx/wpt")
static class Wpt {
public Wpt() {
}

@Tag(path = "/gpx/wpt/desc")
String description;

@Tag(path = "time")
String time;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
import org.assertj.core.api.ThrowableAssert;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Objects;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

public class SetDeserializationTest {
@Test
void deserializeIntoSetField_OfComplexType_ContainingTopLevelMapping() {
Expand Down Expand Up @@ -302,20 +303,19 @@ void deserializeIntoSetField_OfComplexType_ContainingRelativeMappedField() {
""";

// when
var weatherData = new XjxSerdes().read(data, ListDeserializationTest.WeatherDataRelativeMapping.class);
var weatherData = new XjxSerdes().read(data, WeatherDataRelativeMapping.class);

// then
Assertions.assertThat(weatherData.forecasts).hasSize(2);
Assertions.assertThat(weatherData.forecasts.get(0).maxTemperature).isEqualTo("71");
Assertions.assertThat(weatherData.forecasts.get(1).maxTemperature).isEqualTo("78");
Assertions.assertThat(weatherData.forecasts).extracting(r -> r.maxTemperature).containsExactlyInAnyOrder("78", "71");
}

public static class WeatherDataRelativeMapping {
public WeatherDataRelativeMapping() {
}

@Tag(path = "/WeatherData/Forecasts")
Set<ListDeserializationTest.ForecastRelativeMapping> forecasts;
Set<ForecastRelativeMapping> forecasts;
}

@Tag(path = "/WeatherData/Forecasts/Day")
Expand Down Expand Up @@ -361,22 +361,20 @@ void deserializeIntoSetField_OfComplexType_ContainingRelativeAndAbsoluteMappedFi
""";

// when
var weatherData = new XjxSerdes().read(data, ListDeserializationTest.WeatherDataRelativeAndAbsoluteMapping.class);
var weatherData = new XjxSerdes().read(data, WeatherDataRelativeAndAbsoluteMapping.class);

// then
Assertions.assertThat(weatherData.forecasts).hasSize(2);
Assertions.assertThat(weatherData.forecasts.get(0).maxTemperature).isEqualTo("71");
Assertions.assertThat(weatherData.forecasts.get(0).minTemperature).isEqualTo("60");
Assertions.assertThat(weatherData.forecasts.get(1).maxTemperature).isEqualTo("78");
Assertions.assertThat(weatherData.forecasts.get(1).minTemperature).isEqualTo("62");
Assertions.assertThat(weatherData.forecasts).extracting(r -> r.maxTemperature).containsExactlyInAnyOrder("78", "71");
Assertions.assertThat(weatherData.forecasts).extracting(r -> r.minTemperature).containsExactlyInAnyOrder("62", "60");
}

public static class WeatherDataRelativeAndAbsoluteMapping {
public WeatherDataRelativeAndAbsoluteMapping() {
}

@Tag(path = "/WeatherData/Forecasts")
Set<ListDeserializationTest.ForecastRelativeAndAbsoluteMapping> forecasts;
Set<ForecastRelativeAndAbsoluteMapping> forecasts;
}

@Tag(path = "/WeatherData/Forecasts/Day")
Expand All @@ -391,4 +389,61 @@ public ForecastRelativeAndAbsoluteMapping() {
String minTemperature;
}

@Test
void deserializeIntoSetField_whereRootTagContainsRepeatedElements() {
// given
String xmlDoc = """
<gpx
version="1.0"
creator="ExpertGPS 1.1 - https://www.topografix.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.topografix.com/GPX/1/0"
xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd">
<wpt lat="42.438878" lon="-71.119277">
<ele>44.586548</ele>
<time>2001-11-28T21:05:28Z</time>
<name>5066</name>
<desc><![CDATA[5066]]></desc>
<sym>Crossing</sym>
<type><![CDATA[Crossing]]></type>
</wpt>
<wpt lat="42.439227" lon="-71.119689">
<ele>57.607200</ele>
<time>2001-06-02T03:26:55Z</time>
<name>5067</name>
<desc><![CDATA[5067]]></desc>
<sym>Dot</sym>
<type><![CDATA[Intersection]]></type>
</wpt>
</gpx>""";

// when
var gpx = new XjxSerdes().read(xmlDoc, Gpx.class);

// then
assertThat(gpx.wayPoints).hasSize(2);
Assertions.assertThat(gpx.wayPoints).extracting(r -> r.description).containsExactlyInAnyOrder("5066", "5067");
Assertions.assertThat(gpx.wayPoints).extracting(r -> r.time).containsExactlyInAnyOrder("2001-11-28T21:05:28Z", "2001-06-02T03:26:55Z");
}

static class Gpx {
public Gpx() {
}

@Tag(path = "/gpx")
Set<Wpt> wayPoints;
}

@Tag(path = "/gpx/wpt")
static class Wpt {
public Wpt() {
}

@Tag(path = "/gpx/wpt/desc")
String description;

@Tag(path = "time")
String time;
}

}

0 comments on commit 842f65b

Please sign in to comment.