diff --git a/README.md b/README.md index 7eafa54..e683429 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,43 @@ Example XML document: ``` +### Map mixed tags within a container to multiple collections + +Xjx is able to map repeated mixed tags within a container or +at the root tag to multiple collections. + +```xml + + + + + + + + + + + + + +``` + +```java +class WeatherReport { + + @Tag(path = "/WeatherReport/Locations", items = "Town") + List towns; + + @Tag(path = "/WeatherReport/Locations", items = "City") + List cities; +} + +class Town { + @Tag(path = "/WeatherReport/Locations/Town", attribute = "name") + String name; +} +``` + ### Map types Maps can be deserialized either as a field or a top-level type. Consider the following XML document: diff --git a/pom.xml b/pom.xml index f53acab..78c6a82 100644 --- a/pom.xml +++ b/pom.xml @@ -42,14 +42,17 @@ 17 17 + 3.0.0 + 3.10.1 + 3.2.0 + 3.2.1 + 1.5.0 + 3.1.2 + 3.3.0 - 1.5.0 - 3.1.2 - 3.0.0 - 3.10.1 - 3.2.0 - 3.2.1 1.9.0 + 5.10.1 + 3.23.1 @@ -57,14 +60,14 @@ org.junit junit-bom - 5.10.1 + ${junit-bom.version} pom import org.assertj assertj-core - 3.23.1 + ${assertj-core.version} test @@ -212,6 +215,7 @@ org.apache.maven.plugins maven-source-plugin + ${maven-source-plugin.version} attach-sources diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/LazySupplier.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/LazySupplier.java new file mode 100644 index 0000000..539570d --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/LazySupplier.java @@ -0,0 +1,26 @@ +package io.jonasg.xjx.serdes.deserialize; + +import java.util.function.Supplier; + +public class LazySupplier implements Supplier { + private T instance; + private Supplier initializer; + + public LazySupplier(Supplier initializer) { + this.initializer = initializer; + } + + @Override + public T get() { + if (instance == null) { + instance = initializer.get(); + } + return instance; + } + + public void reset(Supplier supplier) { + this.instance = null; + this.initializer = supplier; + } +} + 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 6909396..a180a55 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 @@ -13,7 +13,7 @@ public class PathBasedSaxHandler implements SaxHandler { - private final Function> indexSupplier; + private final Function indexSupplier; private final XjxConfiguration configuration; @@ -23,7 +23,7 @@ public class PathBasedSaxHandler implements SaxHandler { private Path path; - private Map pathWriterIndex; + private PathWriterIndex pathWriterIndex; private String data; @@ -31,12 +31,12 @@ public class PathBasedSaxHandler implements SaxHandler { private String mapStartTag; - public PathBasedSaxHandler(Function> indexSupplier, XjxConfiguration configuration) { + public PathBasedSaxHandler(Function indexSupplier, XjxConfiguration configuration) { this.indexSupplier = indexSupplier; this.configuration = configuration; } - public PathBasedSaxHandler(Function> indexSupplier, String rootTag, XjxConfiguration configuration) { + public PathBasedSaxHandler(Function indexSupplier, String rootTag, XjxConfiguration configuration) { this.indexSupplier = indexSupplier; this.rootTag = rootTag; this.configuration = configuration; @@ -57,24 +57,26 @@ public void startTag(String namespace, String name, List attributes) handleRootTag(name); } else { this.path = path.append(name); - var pathWriter = pathWriterIndex.get(path); - if (pathWriter != null) { - if (pathWriter.getObjectInitializer() != null) { - Object object = pathWriter.getObjectInitializer().get(); - if (object instanceof Map) { - this.mapRootSaxHandlerDelegate = new MapRootSaxHandler((HashMap) object); - this.mapStartTag = name; - } else if (object instanceof MapWithTypeInfo mapWithTypeInfo) { - this.mapRootSaxHandlerDelegate = new TypedValueMapSaxHandler(mapWithTypeInfo, configuration); - this.mapStartTag = name; - } - this.objectInstances.push(object); - } + List pathWriters = pathWriterIndex.get(path); + if (pathWriters != null) { + pathWriters.forEach(pathWriter -> { + if (pathWriter.getObjectInitializer() != null) { + Object object = pathWriter.getObjectInitializer().get(); + if (object instanceof Map) { + this.mapRootSaxHandlerDelegate = new MapRootSaxHandler((HashMap) object); + this.mapStartTag = name; + } else if (object instanceof MapWithTypeInfo mapWithTypeInfo) { + this.mapRootSaxHandlerDelegate = new TypedValueMapSaxHandler(mapWithTypeInfo, configuration); + this.mapStartTag = name; + } + this.objectInstances.push(object); + } + }); } attributes.forEach(a -> { - var attributeWriter = pathWriterIndex.get(path.appendAttribute(a.name())); - if (attributeWriter != null) { - attributeWriter.getValueInitializer().accept(a.value()); + List attributeWriters = pathWriterIndex.get(path.appendAttribute(a.name())); + if (attributeWriters != null) { + attributeWriters.stream().forEach(attributeWriter -> attributeWriter.getValueInitializer().accept(a.value())); } }); } @@ -89,17 +91,19 @@ public void endTag(String namespace, String name) { this.mapRootSaxHandlerDelegate.endTag(namespace, name); } } - PathWriter pathWriter = pathWriterIndex.get(path); - if (pathWriter != null) { - if (data != null) { - pathWriter.getValueInitializer().accept(data); - } - if (pathWriter.getObjectInitializer() != null && !objectInstances.isEmpty() && objectInstances.size() != 1) { - if (pathWriter.getValueInitializer() != null) { - pathWriter.getValueInitializer().accept(objectInstances.peek()); + List pathWriters = pathWriterIndex.get(path); + if (pathWriters != null) { + pathWriters.forEach(pathWriter -> { + if (data != null) { + pathWriter.getValueInitializer().accept(data); } - objectInstances.pop(); - } + if (pathWriter.getObjectInitializer() != null && !objectInstances.isEmpty() && objectInstances.size() != 1) { + if (pathWriter.getValueInitializer() != null) { + pathWriter.getValueInitializer().accept(objectInstances.peek()); + } + objectInstances.pop(); + } + }); } data = null; path = path.pop(); @@ -117,15 +121,17 @@ private void handleRootTag(String name) { this.pathWriterIndex = indexSupplier.apply(name); this.rootTag = name; path = Path.of(name); - PathWriter pathWriter = pathWriterIndex.get(path); - if (pathWriter != null) { - Object parent = pathWriter.getRootInitializer().get(); - if (parent instanceof MapAsRoot mapAsRoot) { - this.mapRootSaxHandlerDelegate = new MapRootSaxHandler(mapAsRoot.map()); - this.objectInstances.push(mapAsRoot.root()); - } else { - this.objectInstances.push(parent); - } + List pathWriters = pathWriterIndex.get(path); + if (pathWriters != null) { + pathWriters.forEach(pathWriter -> { + Object parent = pathWriter.getRootInitializer().get(); + if (parent instanceof MapAsRoot mapAsRoot) { + this.mapRootSaxHandlerDelegate = new MapRootSaxHandler(mapAsRoot.map()); + this.objectInstances.push(mapAsRoot.root()); + } else { + this.objectInstances.push(parent); + } + }); } } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndex.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndex.java new file mode 100644 index 0000000..16e22f9 --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndex.java @@ -0,0 +1,33 @@ +package io.jonasg.xjx.serdes.deserialize; + +import io.jonasg.xjx.serdes.Path; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PathWriterIndex { + + private final Map> index = new HashMap<>(); + + public void put(Path path, PathWriter pathWriter) { + index.compute(path, (p, w) -> { + if (w == null) { + List pathWriters = new ArrayList<>(); + pathWriters.add(pathWriter); + return pathWriters; + } + w.add(pathWriter); + return w; + }); + } + + public void putAll(PathWriterIndex pathWriterIndex) { + index.putAll(pathWriterIndex.index); + } + + public List get(Path path) { + return index.get(path); + } +} 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 bd1bf23..f7d8b53 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 @@ -39,13 +39,13 @@ public PathWriterIndexFactory(XjxConfiguration xjxConfiguration) { this.configuration = xjxConfiguration; } - public Map createIndexForType(Class type, String rootTag) { + public PathWriterIndex createIndexForType(Class type, String rootTag) { Path path = Path.of(rootTag); return buildIndex(type, path); } - private Map buildIndex(Class type, Path path) { - Map index = new HashMap<>(); + private PathWriterIndex buildIndex(Class type, Path path) { + var index = new PathWriterIndex(); if (type.isRecord()) { RecordWrapper recordWrapper = new RecordWrapper<>(type); index.put(path, PathWriter.rootInitializer(() -> recordWrapper)); @@ -57,16 +57,16 @@ private Map buildIndex(Class type, Path path) { } } - private Map doBuildIndex(Class type, + private PathWriterIndex doBuildIndex(Class type, Path path, - Map index, + PathWriterIndex index, Supplier root) { TypeReflector.reflect(type).fields() .forEach(field -> indexField(field, index, path, root)); return index; } - private void indexField(FieldReflector field, Map index, Path path, Supplier parent) { + private void indexField(FieldReflector field, PathWriterIndex index, Path path, Supplier parent) { if (BASIC_TYPES.contains(field.type())) { indexSimpleType(field, index, path, parent); } else if (List.class.equals(field.type())) { @@ -84,7 +84,7 @@ private void indexField(FieldReflector field, Map index, Path } } - private void indexRecordType(FieldReflector field, Map index, Path path, Supplier parent) { + private void indexRecordType(FieldReflector field, PathWriterIndex index, Path path, Supplier parent) { RecordWrapper recordWrapper = new RecordWrapper<>(field.type()); index.put(getPathForField(field, path), PathWriter.objectInitializer(() -> { return recordWrapper; @@ -96,7 +96,7 @@ private void indexRecordType(FieldReflector field, Map index, doBuildIndex(field.type(), getPathForField(field, path), index, () -> recordWrapper); } - private void indexMapType(FieldReflector field, Map index, Path path, Supplier parent) { + private void indexMapType(FieldReflector field, PathWriterIndex index, Path path, Supplier parent) { Path pathForField = getPathForField(field, path); if (pathForField.isRoot()) { indexMapAsRootType(field, index, parent, pathForField); @@ -106,7 +106,7 @@ private void indexMapType(FieldReflector field, Map index, Pat } private void doIndexMapType(FieldReflector field, - Map index, + PathWriterIndex index, Supplier parent, Path pathForField) { index.put(pathForField, PathWriter.objectInitializer(() -> { @@ -122,7 +122,7 @@ private void doIndexMapType(FieldReflector field, } private void indexMapAsRootType(FieldReflector field, - Map index, + PathWriterIndex index, Supplier parent, Path pathForField) { index.put(pathForField, PathWriter.rootInitializer(() -> { @@ -133,7 +133,7 @@ private void indexMapAsRootType(FieldReflector field, } private void indexComplexType(FieldReflector field, - Map index, + PathWriterIndex index, Path path, Supplier parent) { if (field.hasAnnotation(Tag.class)) { @@ -152,7 +152,7 @@ private void indexComplexType(FieldReflector field, } } - private void doIndexComplexType(FieldReflector field, Map index, Path path, Supplier parent) { + private void doIndexComplexType(FieldReflector field, PathWriterIndex index, Path path, Supplier parent) { if (field.hasAnnotation(ValueDeserialization.class)) { index.put(getPathForField(field, path), PathWriter.valueInitializer((value) -> { value = ValueDeserializationHandler.getInstance().handle(field.rawField(), (String) value) @@ -194,7 +194,7 @@ private Optional searchFieldsRecursivelyForTag(FieldReflector field) { return Optional.empty(); } - private void indexEnumType(FieldReflector field, Map index, Path path, Supplier parent) { + private void indexEnumType(FieldReflector field, PathWriterIndex index, Path path, Supplier parent) { index.put(getPathForField(field, path), PathWriter.valueInitializer((value) -> { if (field.hasAnnotation(ValueDeserialization.class)) { value = ValueDeserializationHandler.getInstance().handle(field.rawField(), (String) value) @@ -204,7 +204,7 @@ private void indexEnumType(FieldReflector field, Map index, Pa })); } - private void indexSimpleType(FieldReflector field, Map index, Path path, Supplier parent) { + private void indexSimpleType(FieldReflector field, PathWriterIndex index, Path path, Supplier parent) { if (field.hasAnnotation(Tag.class)) { index.put(getPathForField(field, path), PathWriter.valueInitializer((value) -> { if (value instanceof String) { @@ -216,16 +216,20 @@ private void indexSimpleType(FieldReflector field, Map index, } } - private void indexSetType(FieldReflector field, Map index, Path parentPath, Supplier parent) { - Collection set = new HashSet<>(); + private void indexSetType(FieldReflector field, PathWriterIndex index, Path parentPath, Supplier parent) { + LazySupplier> set = new LazySupplier<>(HashSet::new); Path path = getPathForField(field, parentPath); var pathWriter = PathWriter.objectInitializer(() -> { - FieldAccessor.of(field, parent.get(), configuration).set(set); + var value = set.get(); + if (!value.isEmpty()) { + set.reset(HashSet::new); + } + FieldAccessor.of(field, parent.get(), configuration).set(set.get()); return set; }); if (path.isRoot()) { pathWriter.setRootInitializer(() -> { - FieldAccessor.of(field, parent.get(), configuration).set(set); + FieldAccessor.of(field, parent.get(), configuration).set(set.get()); return parent.get(); }); } @@ -251,16 +255,20 @@ private Supplier collectionSupplierForType(Class typeArgument) { }; } - private void indexListType(FieldReflector field, Map index, Path parentPath, Supplier parent) { - List list = new ArrayList<>(); + private void indexListType(FieldReflector field, PathWriterIndex index, Path parentPath, Supplier parent) { + LazySupplier> list = new LazySupplier<>(ArrayList::new); Path path = getPathForField(field, parentPath); var pathWriter = PathWriter.objectInitializer(() -> { - FieldAccessor.of(field, parent.get(), configuration).set(list); + Collection value = list.get(); + if (!value.isEmpty()) { + list.reset(ArrayList::new); + } + FieldAccessor.of(field, parent.get(), configuration).set(list.get()); return list; }); if (path.isRoot()) { pathWriter.setRootInitializer(() -> { - FieldAccessor.of(field, parent.get(), configuration).set(list); + FieldAccessor.of(field, parent.get(), configuration).set(list.get()); return parent.get(); }); } @@ -269,7 +277,7 @@ private void indexListType(FieldReflector field, Map index, Pa indexListTypeArgument(path, field, index, list); } - private void indexListTypeArgument(Path path, FieldReflector field, Map index, Collection list) { + private void indexListTypeArgument(Path path, FieldReflector field, PathWriterIndex index, LazySupplier> list) { Type actualTypeArgument = ((ParameterizedType) field.genericType()).getActualTypeArguments()[0]; Class typeArgument = (Class) actualTypeArgument; if (TypeMappers.TYPES.contains(typeArgument)) { @@ -279,13 +287,13 @@ private void indexListTypeArgument(Path path, FieldReflector field, Map index, Collection list, FieldReflector field, Class typeArgument) { + private void indexSimpleTypeListTypeArgument(Path path, PathWriterIndex index, LazySupplier> list, FieldReflector field, Class typeArgument) { Tag tag = field.getAnnotation(Tag.class); index.put(path.append(Path.parse(tag.items())), - PathWriter.valueInitializer((o) -> list.add(TypeMappers.forType(typeArgument, configuration).apply(o)))); + PathWriter.valueInitializer((o) -> list.get().add(TypeMappers.forType(typeArgument, configuration).apply(o)))); } - private void indexComplexListTypeArgument(Map index, Collection list, Class typeArgument, FieldReflector field) { + private void indexComplexListTypeArgument(PathWriterIndex index, LazySupplier> list, Class typeArgument, FieldReflector field) { Supplier listTypeInstanceSupplier = collectionSupplierForType(typeArgument); Tag tag = field.getAnnotation(Tag.class); if (tag.items().isBlank()) { @@ -298,12 +306,12 @@ private void indexComplexListTypeArgument(Map index, Collectio index.put(path, PathWriter.objectInitializer(() -> { collectionCacheType.clear(); Object listTypeInstance = listTypeInstanceSupplier.get(); - list.add(listTypeInstance); + list.get().add(listTypeInstance); return listTypeInstance; }).setValueInitializer((value) -> { if (value instanceof RecordWrapper recordWrapperValue) { - list.remove(recordWrapperValue); - list.add(recordWrapperValue.record()); + list.get().remove(recordWrapperValue); + list.get().add(recordWrapperValue.record()); } })); doBuildIndex(typeArgument, path, index, listTypeInstanceSupplier); 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 8467135..05a2129 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 @@ -5,13 +5,17 @@ import java.time.LocalDate; import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import io.jonasg.xjx.serdes.Tag; import io.jonasg.xjx.serdes.XjxSerdes; +@SuppressWarnings("unused") public class ListDeserializationTest { @Test @@ -600,6 +604,313 @@ record WeatherReport(@Tag(path = "/WeatherReport", items = "City") List + + + + + + + + + + + + + """; + XjxSerdes xjx = new XjxSerdes(); + var weatherReport = xjx.read(data, WeatherReport.class); + + assertThat(weatherReport.cities) + .hasSize(4) + .containsExactly(new City("A"), new City("F"), new City("H"), new City("G")); + assertThat(weatherReport.towns) + .hasSize(4) + .containsExactly(new Town("B"), new Town("D"), new Town("E"), new Town("C")); + } + + @Test + void atRootLevel() { + String data = """ + + + + + + + + + + + + """; + XjxSerdes xjx = new XjxSerdes(); + var weatherReport = xjx.read(data, WeatherReportAtRoot.class); + + assertThat(weatherReport.cities) + .hasSize(4) + .containsExactly(new CityAtRoot("A"), new CityAtRoot("F"), new CityAtRoot("H"), new CityAtRoot("G")); + assertThat(weatherReport.towns) + .hasSize(4) + .containsExactly(new TownAtRoot("B"), new TownAtRoot("D"), new TownAtRoot("E"), new TownAtRoot("C")); + } + + @Test + void repeatedTagContainingCollection() { + String data = """ + + + + + + + + + + + + + + """; + XjxSerdes xjx = new XjxSerdes(); + Manifest manifest = xjx.read(data, Manifest.class); + + assertThat(manifest.foos) + .hasSize(2) + .containsExactly(new Foo("A"), new Foo("B")); + assertThat(manifest.bars) + .hasSize(2) + .containsExactly( + new Bar("X", List.of(new Info("InfoValX1"), new Info("InfoValX2"))), + new Bar("Z", List.of(new Info("InfoValZ1")))); + } + } + + private static class Manifest { + @Tag(path = "/manifest", items = "foo") + private List foos; + + @Tag(path = "/manifest", items = "bar") + private List bars; + } + + private static class Foo { + @Tag(path = "/manifest/foo", attribute = "name") + private String name; + + public Foo() { + } + + public Foo(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Foo foo = (Foo) o; + return Objects.equals(name, foo.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + private static class Bar { + @Tag(path = "/manifest/bar", attribute = "name") + private String name; + + @Tag(path = "/manifest/bar", items = "info") + private List infos; + + public Bar() { + } + + public Bar(String name, List infos) { + this.name = name; + this.infos = infos; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Bar bar = (Bar) o; + return Objects.equals(name, bar.name) && Objects.equals(infos, bar.infos); + } + + @Override + public int hashCode() { + return Objects.hash(name, infos); + } + + @Override + public String toString() { + return new StringJoiner(", ", Bar.class.getSimpleName() + "[", "]") + .add("name='" + name + "'") + .add("infos=" + infos) + .toString(); + } + } + + private static class Info { + @Tag(path = "/manifest/bar/info", attribute = "val") + private String val; + + public Info() { + } + + public Info(String val) { + this.val = val; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Info info = (Info) o; + return Objects.equals(val, info.val); + } + + @Override + public int hashCode() { + return Objects.hash(val); + } + } + + static class TownAtRoot { + public TownAtRoot() { + } + + @Tag(path = "/WeatherReport/Town", attribute = "name") + String name; + + public TownAtRoot(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TownAtRoot townAtRoot = (TownAtRoot) o; + return Objects.equals(name, townAtRoot.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + static class CityAtRoot { + public CityAtRoot() { + } + + @Tag(path = "/WeatherReport/City", attribute = "name") + String name; + + public CityAtRoot(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CityAtRoot cityAtRoot = (CityAtRoot) o; + return Objects.equals(name, cityAtRoot.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + static class WeatherReportAtRoot { + + public WeatherReportAtRoot() { + } + + @Tag(path = "/WeatherReport", items = "Town") + List towns; + + @Tag(path = "/WeatherReport", items = "City") + List cities; + } + + static class Town { + public Town() { + } + + @Tag(path = "/WeatherReport/Locations/Town", attribute = "name") + String name; + + public Town(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Town townAtRoot = (Town) o; + return Objects.equals(name, townAtRoot.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + static class City { + public City() { + } + + @Tag(path = "/WeatherReport/Locations/City", attribute = "name") + String name; + + public City(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + City cityAtRoot = (City) o; + return Objects.equals(name, cityAtRoot.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + static class WeatherReport { + + public WeatherReport() { + } + + @Tag(path = "/WeatherReport/Locations", items = "Town") + List towns; + + @Tag(path = "/WeatherReport/Locations", items = "City") + List cities; + } + static class Gpx { public Gpx() { } 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 4b8e439..fde8206 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 @@ -4,18 +4,23 @@ import java.util.Objects; import java.util.Set; +import java.util.StringJoiner; import org.assertj.core.api.Assertions; +import org.assertj.core.api.Condition; import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import io.jonasg.xjx.serdes.Tag; import io.jonasg.xjx.serdes.XjxSerdes; +import io.jonasg.xjx.serdes.deserialize.ListDeserializationTest.CityAtRoot; +import io.jonasg.xjx.serdes.deserialize.ListDeserializationTest.TownAtRoot; public class SetDeserializationTest { @Test - void deserializeIntoListField_OfStringType() { + void deserializeIntoSetField_OfStringType() { // given String data = """ @@ -30,7 +35,7 @@ void deserializeIntoListField_OfStringType() { """; // when - var dataHolder = new XjxSerdes().read(data, ListOfStrings.class); + var dataHolder = new XjxSerdes().read(data, SetOfStrings.class); // then assertThat(dataHolder.strings).containsExactlyInAnyOrder( @@ -41,7 +46,7 @@ void deserializeIntoListField_OfStringType() { ); } - static class ListOfStrings { + static class SetOfStrings { @Tag(path = "/Data/Strings", items = "String") Set strings; } @@ -492,6 +497,286 @@ record WeatherReport( new CityWeather("Sunny", "New York")); } + @Nested + class RepeatedTagsMappedToMultipleSetsTest { + + @Test + void wrappedInContainerTag() { + String data = """ + + + + + + + + + + + + + + """; + XjxSerdes xjx = new XjxSerdes(); + var weatherReport = xjx.read(data, WeatherReport.class); + + assertThat(weatherReport.cities) + .hasSize(4) + .contains(new City("A"), new City("F"), new City("H"), new City("G")); + assertThat(weatherReport.towns) + .hasSize(4) + .contains(new Town("B"), new Town("D"), new Town("E"), new Town("C")); + } + + @Test + void atRootLevel() { + String data = """ + + + + + + + + + + + + """; + XjxSerdes xjx = new XjxSerdes(); + var weatherReport = xjx.read(data, WeatherReportAtRoot.class); + + assertThat(weatherReport.cities) + .hasSize(4) + .contains(new CityAtRoot("A"), new CityAtRoot("F"), new CityAtRoot("H"), new CityAtRoot("G")); + assertThat(weatherReport.towns) + .hasSize(4) + .contains(new TownAtRoot("B"), new TownAtRoot("D"), new TownAtRoot("E"), new TownAtRoot("C")); + } + + @Test + void repeatedTagContainingCollection() { + String data = """ + + + + + + + + + + + + + + """; + XjxSerdes xjx = new XjxSerdes(); + Manifest manifest = xjx.read(data, Manifest.class); + + assertThat(manifest.foos) + .hasSize(2) + .contains(new Foo("A"), new Foo("B")); + assertThat(manifest.bars) + .hasSize(2); + assertThat(manifest.bars).extracting("name") + .containsExactlyInAnyOrder("X", "Z"); + + Bar barX = manifest.bars.stream() + .filter(b -> "X".equals(b.name)) + .findFirst() + .orElseThrow(); + + assertThat(barX.infos).extracting("val") + .containsExactlyInAnyOrder("InfoValX1", "InfoValX2"); + + Bar barZ = manifest.bars.stream() + .filter(b -> "Z".equals(b.name)) + .findFirst() + .orElseThrow(); + + assertThat(barZ.infos).extracting("val") + .containsExactlyInAnyOrder("InfoValZ1"); + } + } + + + private static class Manifest { + @Tag(path = "/manifest", items = "foo") + private Set foos; + + @Tag(path = "/manifest", items = "bar") + private Set bars; + } + + private static class Foo { + @Tag(path = "/manifest/foo", attribute = "name") + private String name; + + public Foo() { + } + + public Foo(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Foo foo = (Foo) o; + return Objects.equals(name, foo.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + private static class Bar { + @Tag(path = "/manifest/bar", attribute = "name") + private String name; + + @Tag(path = "/manifest/bar", items = "info") + private Set infos; + + public Bar() { + } + + public Bar(String name, Set infos) { + this.name = name; + this.infos = infos; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Bar bar = (Bar) o; + return Objects.equals(name, bar.name) && + Objects.equals(infos, bar.infos); + } + + @Override + public int hashCode() { + return Objects.hash(name, infos); + } + + @Override + public String toString() { + return new StringJoiner(", ", Bar.class.getSimpleName() + "[", "]") + .add("name='" + name + "'") + .add("infos=" + infos) + .toString(); + } + } + + private static class Info { + @Tag(path = "/manifest/bar/info", attribute = "val") + private String val; + + public Info() { + } + + public Info(String val) { + this.val = val; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Info info = (Info) o; + return Objects.equals(val, info.val); + } + + @Override + public int hashCode() { + return Objects.hash(val); + } + + @Override + public String toString() { + return new StringJoiner(", ", Info.class.getSimpleName() + "[", "]") + .add("val='" + val + "'") + .toString(); + } + } + + static class WeatherReport { + + public WeatherReport() { + } + + @Tag(path = "/WeatherReport/Locations", items = "Town") + Set towns; + + @Tag(path = "/WeatherReport/Locations", items = "City") + Set cities; + } + + static class City { + public City() { + } + + @Tag(path = "/WeatherReport/Locations/City", attribute = "name") + String name; + + public City(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + City city = (City) o; + return Objects.equals(name, city.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + static class Town { + public Town() { + } + + @Tag(path = "/WeatherReport/Locations/Town", attribute = "name") + String name; + + public Town(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Town town = (Town) o; + return Objects.equals(name, town.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + static class WeatherReportAtRoot { + public WeatherReportAtRoot() { + } + + @Tag(path = "/WeatherReport", items = "Town") + Set towns; + + @Tag(path = "/WeatherReport", items = "City") + Set cities; + } + static class Gpx { public Gpx() { }