diff --git a/.gitignore b/.gitignore index ad52d6f..7bef1e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +**big.xml +**gpx.xml target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ diff --git a/README.md b/README.md index 41e9995..7d9c725 100644 --- a/README.md +++ b/README.md @@ -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. 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 9f14467..a17e69f 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 @@ -23,6 +23,10 @@ public static PathWriter objectInitializer(Supplier objectInitializer) { return pathWriter; } + public void setRootInitializer(Supplier rootInitializer) { + this.rootInitializer = rootInitializer; + } + 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 dc2042a..5e81ff3 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 @@ -175,26 +175,34 @@ private void indexSimpleType(FieldReflector field, Map index, } } - private void indexSetType(FieldReflector field, Map index, Path path, Supplier parent) { + private void indexSetType(FieldReflector field, Map index, Path parentPath, Supplier parent) { Collection 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 listTypeInstanceSupplier = collectionSupplierForType(type); + Supplier 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() + ")"); } } @@ -211,10 +219,18 @@ private Supplier collectionSupplierForType(Class typeArgument) { private void indexListType(FieldReflector field, Map index, Path parentPath, Supplier parent) { List 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); diff --git a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/ListDeserializationTest.java b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/ListDeserializationTest.java index 5bb7591..1df967c 100644 --- a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/ListDeserializationTest.java +++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/ListDeserializationTest.java @@ -8,6 +8,8 @@ import java.util.List; +import static org.assertj.core.api.Assertions.*; + public class ListDeserializationTest { @Test @@ -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 { @@ -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 @@ -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 @@ -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 { @@ -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)"); } @@ -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 { @@ -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 { @@ -384,4 +386,63 @@ public ForecastRelativeAndAbsoluteMapping() { @Tag(path = "/WeatherData/Forecasts/Day/Low/Value") String minTemperature; } + + @Test + void deserializeIntoListField_whereRootTagContainsRepeatedElements() { + // given + String xmlDoc = """ + + + 44.586548 + + 5066 + + Crossing + + + + 57.607200 + + 5067 + + Dot + + + """; + + // 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 wayPoints; + } + + @Tag(path = "/gpx/wpt") + static class Wpt { + public Wpt() { + } + + @Tag(path = "/gpx/wpt/desc") + String description; + + @Tag(path = "time") + String time; + } } diff --git a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/SetDeserializationTest.java b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/SetDeserializationTest.java index b6e99e0..229852b 100644 --- a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/SetDeserializationTest.java +++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/SetDeserializationTest.java @@ -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() { @@ -302,12 +303,11 @@ 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 { @@ -315,7 +315,7 @@ public WeatherDataRelativeMapping() { } @Tag(path = "/WeatherData/Forecasts") - Set forecasts; + Set forecasts; } @Tag(path = "/WeatherData/Forecasts/Day") @@ -361,14 +361,12 @@ 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 { @@ -376,7 +374,7 @@ public WeatherDataRelativeAndAbsoluteMapping() { } @Tag(path = "/WeatherData/Forecasts") - Set forecasts; + Set forecasts; } @Tag(path = "/WeatherData/Forecasts/Day") @@ -391,4 +389,61 @@ public ForecastRelativeAndAbsoluteMapping() { String minTemperature; } + @Test + void deserializeIntoSetField_whereRootTagContainsRepeatedElements() { + // given + String xmlDoc = """ + + + 44.586548 + + 5066 + + Crossing + + + + 57.607200 + + 5067 + + Dot + + + """; + + // 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 wayPoints; + } + + @Tag(path = "/gpx/wpt") + static class Wpt { + public Wpt() { + } + + @Tag(path = "/gpx/wpt/desc") + String description; + + @Tag(path = "time") + String time; + } + }