From 2f21ca6e9e89ee69304e4b3cd15269c7a56fb338 Mon Sep 17 00:00:00 2001
From: JonasG
Date: Mon, 15 Jan 2024 21:39:45 +0100
Subject: [PATCH] feat: support simple generic arguments for List and Set
mappings
---
README.md | 87 ++---
.../xjx/serdes/{deserialize => }/Path.java | 4 +-
.../java/io/jonasg/xjx/serdes/Section.java | 33 +-
.../main/java/io/jonasg/xjx/serdes/Tag.java | 62 ++++
.../io/jonasg/xjx/serdes/TypeMappers.java | 89 +++++
.../java/io/jonasg/xjx/serdes/XjxSerdes.java | 2 +
.../deserialize/PathBasedSaxHandler.java | 1 +
.../deserialize/PathWriterIndexFactory.java | 114 +++++--
.../deserialize/accessor/FieldAccessor.java | 68 +---
.../XmlNodeStructureFactory.java | 7 +-
.../{ => seraialize}/XmlStringBuilder.java | 2 +-
.../deserialize/ListDeserializationTest.java | 308 +++++++++++++-----
.../deserialize/SetDeserializationTest.java | 51 ++-
.../GeneralSerializationTest.java | 2 +-
.../TagAttributeSerializationTest.java | 2 +-
15 files changed, 549 insertions(+), 283 deletions(-)
rename xjx-serdes/src/main/java/io/jonasg/xjx/serdes/{deserialize => }/Path.java (97%)
create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/TypeMappers.java
rename xjx-serdes/src/main/java/io/jonasg/xjx/serdes/{ => seraialize}/XmlNodeStructureFactory.java (91%)
rename xjx-serdes/src/main/java/io/jonasg/xjx/serdes/{ => seraialize}/XmlStringBuilder.java (97%)
rename xjx-serdes/src/test/java/io/jonasg/xjx/serdes/{seraialize => serialize}/GeneralSerializationTest.java (98%)
rename xjx-serdes/src/test/java/io/jonasg/xjx/serdes/{seraialize => serialize}/TagAttributeSerializationTest.java (99%)
diff --git a/README.md b/README.md
index 887dc5e..8fac05e 100644
--- a/README.md
+++ b/README.md
@@ -49,10 +49,10 @@ public class Location {
private Location() {
}
- @Tag(path = "/WeatherData/Location/City")
+ @Tag(path = "City")
private String City;
- @Tag(path = "/WeatherData/Location/Country")
+ @Tag(path = "Country")
private String Country;
}
```
@@ -68,7 +68,7 @@ String document = """
75
- °F
+
60
@@ -134,71 +134,56 @@ class Temperature {
### Collection types
-When deserializing XML data containing a collection type, the following conventions apply:
+When deserializing an XML document containing repeated elements, it can be mapped onto one of the collection types `List` or `Set`.
+
+The following conventions should be followed:
-- Only `List` and `Set` types are supported
-- The List or Set field should be annotated with `@Tag` having a `path` pointing to the containing tag that holds the repeated tags.
-- The nested complex type should be annotated top-level with `@Tag` having a `path` pointing to a single element that is repeated
-- Fields within the nested complex type can be annotated as usual.
+- Only `List` and `Set` types are supported for mapping repeated elements.
+- The `@Tag` annotation should be used on a `List` or `Set` field.
+ - Include a `path` attribute pointing to the containing tag that holds the repeated tags.
+ - Include an `items` attribute pointing to the repeated tag, relatively.
+ - The `path` attribute supports both relative and absolute paths.
+- The generic argument can be any standard simple type (e.g., `String`, `Boolean`, `Double`, `Long`, etc.) or a custom complex type.
+- Fields within the nested complex type can be annotated as usual, using relative or absolute paths.
+
+Example XML document:
```xml
-
-
-
- 71
-
-
- 62
-
-
+
+
+
+ 71
+
+
+ 62
+
+
-
- 78
-
-
- 71
-
+
+ 78
+
+
+ 71
+
```
-```java
-public class WeatherData {
- // When mapping List or Set the type needs to point to the
- // tag containing the repeated elements
- @Tag(path = "/WeatherData/Forecasts")
- List forecasts;
-}
-
-// Top level annoation is required and
-// needs to point to an indiviual element that is repeated
-@Tag(path = "/WeatherData/Forecasts/Day")
-public class Forecast {
- // field can be both absolutely as relatively mapped
- @Tag(path = "High/Value")
- String maxTemperature;
-
- // field can be both absolutely as relatively mapped
- @Tag(path = "/WeatherData/Forecasts/Day/Low/Value")
- String minTemperature;
-}
-```
-
### Map types
Maps can be deserialized either as a field or a top-level type. Consider the following XML document:
```xml
-
-
- 75
- °F
-
-
+
+
+ 75
+ °F
+
+
```
diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/Path.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Path.java
similarity index 97%
rename from xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/Path.java
rename to xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Path.java
index a431d96..18f9169 100644
--- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/Path.java
+++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Path.java
@@ -1,6 +1,4 @@
-package io.jonasg.xjx.serdes.deserialize;
-
-import io.jonasg.xjx.serdes.Section;
+package io.jonasg.xjx.serdes;
import java.util.Arrays;
import java.util.Iterator;
diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Section.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Section.java
index 42639cd..3a599b6 100644
--- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Section.java
+++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Section.java
@@ -1,35 +1,4 @@
package io.jonasg.xjx.serdes;
-import java.util.StringJoiner;
-
-public class Section {
-
- private final String name;
- private final boolean isLeaf;
-
- public Section(String name) {
- this.name = name;
- isLeaf = false;
- }
-
- public Section(String name, boolean isLeaf) {
- this.name = name;
- this.isLeaf = isLeaf;
- }
-
- public String name() {
- return name;
- }
-
- public boolean isLeaf() {
- return isLeaf;
- }
-
- @Override
- public String toString() {
- return new StringJoiner(", ", Section.class.getSimpleName() + "[", "]")
- .add("name='" + name + "'")
- .add("isLeaf=" + isLeaf)
- .toString();
- }
+public record Section(String name, boolean isLeaf) {
}
diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Tag.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Tag.java
index f73f6a3..90f6a2a 100644
--- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Tag.java
+++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Tag.java
@@ -5,10 +5,72 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+/**
+ * The {@code Tag} annotation is used to mark a field for XML serialization and deserialization.
+ * It provides information about the XML path and optional attributes to be used during serialization and deserialization.
+ *
+ *
+ * Example XML document:
+ *
{@code
+ *
+ * Product 1
+ * Product 2
+ * Product 3
+ *
+ * }
+ *
+ *
+ * Example Usage:
+ *
{@code
+ * @Tag(path = "/Products", items = "Name")
+ * List productNames;
+ * }
+ * In this example, the {@code List} field 'productNames' will be serialized to and deserialized from the XML path "/Products/Name".
+ *
+ *
+ *
+ * Example XML for Serialization:
+ *
{@code
+ *
+ * Product 1
+ * Product 2
+ * Product 3
+ *
+ * }
+ * In this example, when the {@code List} field 'productNames' is serialized, the generated XML will look like the above representation.
+ *
+ *
+ *
+ * Annotation Usage:
+ *
+ * {@code path}: Specifies the Path expression indicating the location of the XML data for serialization and deserialization.
+ * {@code attribute}: Specifies the name of an XML attribute to be used during serialization and deserialization (optional).
+ * {@code items}: Specifies additional information for serializing and deserializing items within a collection (optional).
+ *
+ *
+ *
+ */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Tag {
+ /**
+ * Specifies the Path expression indicating the location of the XML data for serialization and deserialization.
+ *
+ * @return The Path expression representing the location of the XML data.
+ */
String path();
+ /**
+ * Specifies the name of an XML attribute to be used during serialization and deserialization (optional).
+ *
+ * @return The name of the XML attribute.
+ */
String attribute() default "";
+
+ /**
+ * Specifies additional information for serializing and deserializing items within a collection (optional).
+ *
+ * @return Additional information for serializing and deserializing items.
+ */
+ String items() default "";
}
diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/TypeMappers.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/TypeMappers.java
new file mode 100644
index 0000000..9d5e2ca
--- /dev/null
+++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/TypeMappers.java
@@ -0,0 +1,89 @@
+package io.jonasg.xjx.serdes;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+public final class TypeMappers {
+
+ static List> DOUBLE_TYPES = List.of(double.class, Double.class);
+ static List> LONG_TYPES = List.of(long.class, Long.class);
+ static List> CHAR_TYPES = List.of(char.class, Character.class);
+ static List> BOOLEAN_TYPES = List.of(boolean.class, Boolean.class);
+
+ public static Set> TYPES;
+
+ static {
+ TYPES = new HashSet<>();
+ TYPES.addAll(DOUBLE_TYPES);
+ TYPES.addAll(LONG_TYPES);
+ TYPES.addAll(CHAR_TYPES);
+ TYPES.addAll(BOOLEAN_TYPES);
+ TYPES.add(String.class);
+ TYPES.add(LocalDate.class);
+ }
+
+ public static Function forType(Class> type) {
+ Function mapper = Function.identity();
+ if (type.equals(String.class)) {
+ mapper = String::valueOf;
+ }
+ if (type.equals(Integer.class)) {
+ mapper = value -> Integer.parseInt(String.valueOf(value));
+ }
+ if (LONG_TYPES.contains(type)) {
+ mapper = value -> Long.parseLong(String.valueOf(value));
+ }
+ if (type.equals(BigDecimal.class)) {
+ mapper = value -> new BigDecimal(String.valueOf(value));
+ }
+ if (DOUBLE_TYPES.contains(type)) {
+ mapper = value -> Double.valueOf(String.valueOf(value));
+ }
+ if (CHAR_TYPES.contains(type)) {
+ mapper = value -> String.valueOf(value).charAt(0);
+ }
+ if (BOOLEAN_TYPES.contains(type)) {
+ mapper = value -> {
+ String lowered = String.valueOf(value).toLowerCase();
+ if (lowered.equals("true") || lowered.equals("yes") || lowered.equals("1")) {
+ return true;
+ }
+ return false;
+ };
+ }
+ if (type.equals(LocalDate.class)) {
+ mapper = value -> LocalDate.parse(String.valueOf(value));
+ }
+ if (type.equals(LocalDateTime.class)) {
+ mapper = value -> LocalDateTime.parse(String.valueOf(value));
+ }
+ if (type.equals(ZonedDateTime.class)) {
+ mapper = value -> ZonedDateTime.parse(String.valueOf(value));
+ }
+ if (type.isEnum()) {
+ mapper = value -> toEnum(type, String.valueOf(value));
+ }
+ return mapper;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static > T toEnum(Class> type, String value) {
+ try {
+ T[] enumConstants = (T[]) type.getEnumConstants();
+ for (T constant : enumConstants) {
+ if (value.equals(constant.name())) {
+ return constant;
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ }
+}
diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java
index 4b95216..2f796fb 100644
--- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java
+++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java
@@ -6,6 +6,8 @@
import io.jonasg.xjx.serdes.deserialize.PathBasedSaxHandler;
import io.jonasg.xjx.serdes.deserialize.PathWriterIndexFactory;
import io.jonasg.xjx.serdes.deserialize.XjxDeserializationException;
+import io.jonasg.xjx.serdes.seraialize.XmlNodeStructureFactory;
+import io.jonasg.xjx.serdes.seraialize.XmlStringBuilder;
import java.io.Reader;
import java.io.StringReader;
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 c1049b9..f992917 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
@@ -2,6 +2,7 @@
import io.jonasg.xjx.sax.Attribute;
import io.jonasg.xjx.sax.SaxHandler;
+import io.jonasg.xjx.serdes.Path;
import java.util.HashMap;
import java.util.LinkedList;
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 5e81ff3..b7b1820 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
@@ -1,9 +1,10 @@
package io.jonasg.xjx.serdes.deserialize;
+import io.jonasg.xjx.serdes.Path;
import io.jonasg.xjx.serdes.Tag;
+import io.jonasg.xjx.serdes.TypeMappers;
import io.jonasg.xjx.serdes.deserialize.accessor.FieldAccessor;
import io.jonasg.xjx.serdes.reflector.FieldReflector;
-import io.jonasg.xjx.serdes.reflector.Reflector;
import io.jonasg.xjx.serdes.reflector.TypeReflector;
import java.lang.reflect.Field;
@@ -42,7 +43,10 @@ private Map buildIndex(Class type, Path path) {
return doBuildIndex(type, path, index, () -> root);
}
- private Map doBuildIndex(Class> type, Path path, Map index, Supplier root) {
+ private Map doBuildIndex(Class> type,
+ Path path,
+ Map index,
+ Supplier root) {
TypeReflector.reflect(type).fields()
.forEach(field -> indexField(field, index, path, root));
return index;
@@ -73,7 +77,10 @@ private void indexMapType(FieldReflector field, Map index, Pat
}
}
- private static void doIndexMapType(FieldReflector field, Map index, Supplier parent, Path pathForField) {
+ private static void doIndexMapType(FieldReflector field,
+ Map index,
+ Supplier parent,
+ Path pathForField) {
index.put(pathForField, PathWriter.objectInitializer(() -> {
Map map = new HashMap<>();
Class> valueType = (Class>) ((ParameterizedType) field.genericType()).getActualTypeArguments()[1];
@@ -86,7 +93,10 @@ private static void doIndexMapType(FieldReflector field, Map i
}));
}
- private static void indexMapAsRootType(FieldReflector field, Map index, Supplier parent, Path pathForField) {
+ private static void indexMapAsRootType(FieldReflector field,
+ Map index,
+ Supplier parent,
+ Path pathForField) {
index.put(pathForField, PathWriter.rootInitializer(() -> {
Map map = new HashMap<>();
FieldAccessor.of(field, parent.get()).set(map);
@@ -94,7 +104,10 @@ private static void indexMapAsRootType(FieldReflector field, Map index, Path path, Supplier parent) {
+ private void indexComplexType(FieldReflector field,
+ Map index,
+ Path path,
+ Supplier parent) {
if (field.hasAnnotation(Tag.class)) {
doIndexComplexType(field, index, path, parent);
} else {
@@ -176,34 +189,49 @@ private void indexSimpleType(FieldReflector field, Map index,
}
private void indexSetType(FieldReflector field, Map index, Path parentPath, Supplier parent) {
+// Collection set = new HashSet<>();
+// 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> typeArgument = (Class>) actualTypeArgument;
+// var tag = Reflector.reflect(typeArgument).annotation(Tag.class);
+// if (tag != null) {
+// Supplier listTypeInstanceSupplier = collectionSupplierForType(typeArgument);
+// index.put(Path.parse(tag.path()), PathWriter.objectInitializer(() -> {
+// collectionCacheType.clear();
+// Object listTypeInstance = listTypeInstanceSupplier.get();
+// set.add(listTypeInstance);
+// return listTypeInstance;
+// }));
+// doBuildIndex(typeArgument, Path.parse(tag.path()), index, listTypeInstanceSupplier);
+// } else {
+// throw new XjxDeserializationException("Generics of type Set require @Tag pointing to mapped XML path (" + typeArgument.getSimpleName() + ")");
+// }
Collection set = new HashSet<>();
Path path = getPathForField(field, parentPath);
var pathWriter = PathWriter.objectInitializer(() -> {
FieldAccessor.of(field, parent.get()).set(set);
return set;
});
- if (parentPath.isRoot()) {
+ if (path.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> typeArgument = (Class>) actualTypeArgument;
- var tag = Reflector.reflect(typeArgument).annotation(Tag.class);
- if (tag != null) {
- Supplier listTypeInstanceSupplier = collectionSupplierForType(typeArgument);
- index.put(Path.parse(tag.path()), PathWriter.objectInitializer(() -> {
- collectionCacheType.clear();
- Object listTypeInstance = listTypeInstanceSupplier.get();
- set.add(listTypeInstance);
- return listTypeInstance;
- }));
- doBuildIndex(typeArgument, Path.parse(tag.path()), index, listTypeInstanceSupplier);
- } else {
- throw new XjxDeserializationException("Generics of type Set require @Tag pointing to mapped XML path (" + typeArgument.getSimpleName() + ")");
- }
+
+ indexListTypeArgument(path, field, index, set);
}
private Supplier collectionSupplierForType(Class> typeArgument) {
@@ -231,23 +259,45 @@ private void indexListType(FieldReflector field, Map index, Pa
});
}
index.put(path, pathWriter);
+
+ indexListTypeArgument(path, field, index, list);
+ }
+
+ private void indexListTypeArgument(Path path, FieldReflector field, Map index, Collection list) {
Type actualTypeArgument = ((ParameterizedType) field.genericType()).getActualTypeArguments()[0];
Class> typeArgument = (Class>) actualTypeArgument;
- var tag = Reflector.reflect(typeArgument).annotation(Tag.class);
- if (tag != null) {
- Supplier listTypeInstanceSupplier = collectionSupplierForType(typeArgument);
- index.put(Path.parse(tag.path()), PathWriter.objectInitializer(() -> {
- collectionCacheType.clear();
- Object listTypeInstance = listTypeInstanceSupplier.get();
- list.add(listTypeInstance);
- return listTypeInstance;
- }));
- doBuildIndex(typeArgument, Path.parse(tag.path()), index, listTypeInstanceSupplier);
+ if (TypeMappers.TYPES.contains(typeArgument)) {
+ indexSimpleTypeListTypeArgument(path, index, list, field, typeArgument);
} else {
- throw new XjxDeserializationException("Generics of type List require @Tag pointing to mapped XML path (" + typeArgument.getSimpleName() + ")");
+ indexComplexListTypeArgument(index, list, typeArgument, field);
}
}
+ private static void indexSimpleTypeListTypeArgument(Path path, Map index, Collection 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).apply(o))));
+ }
+
+ private void indexComplexListTypeArgument(Map index, Collection list, Class> typeArgument, FieldReflector field) {
+ Supplier listTypeInstanceSupplier = collectionSupplierForType(typeArgument);
+ Tag tag = field.getAnnotation(Tag.class);
+ if (tag.items().isBlank()) {
+ throw new XjxDeserializationException(
+ """
+ Field (%s) requires @Tag to have items parameter describing\
+ the tag name of a single repeated tag""".formatted(typeArgument.getSimpleName(), field.name()));
+ }
+ Path path = Path.parse(tag.path()).append(Path.parse(tag.items()));
+ index.put(path, PathWriter.objectInitializer(() -> {
+ collectionCacheType.clear();
+ Object listTypeInstance = listTypeInstanceSupplier.get();
+ list.add(listTypeInstance);
+ return listTypeInstance;
+ }));
+ doBuildIndex(typeArgument, path, index, listTypeInstanceSupplier);
+ }
+
private Path getPathForField(FieldReflector field, Path path) {
Tag tag = field.getAnnotation(Tag.class);
if (tag != null) {
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 d51e6f9..dc1603c 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,83 +1,19 @@
package io.jonasg.xjx.serdes.deserialize.accessor;
+import io.jonasg.xjx.serdes.TypeMappers;
import io.jonasg.xjx.serdes.reflector.FieldReflector;
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.ZonedDateTime;
-import java.util.List;
-import java.util.function.Function;
-
public interface FieldAccessor {
- List> DOUBLE_TYPES = List.of(double.class, Double.class);
- List> LONG_TYPES = List.of(long.class, Long.class);
- List> CHAR_TYPES = List.of(char.class, Character.class);
- List> BOOLEAN_TYPES = List.of(boolean.class, Boolean.class);
-
static FieldAccessor of(FieldReflector field, Object instance) {
var setterFieldAccessor = new SetterFieldAccessor(field, instance); //TODO optimize
if (setterFieldAccessor.hasSetterForField()) {
return new SetterFieldAccessor(field, instance);
}
- Function mapper = Function.identity();
- if (field.type().equals(String.class)) {
- mapper = String::valueOf;
- }
- if (field.type().equals(Integer.class)) {
- mapper = value -> Integer.parseInt(String.valueOf(value));
- }
- if (LONG_TYPES.contains(field.type())) {
- mapper = value -> Long.parseLong(String.valueOf(value));
- }
- if (field.type().equals(BigDecimal.class)) {
- mapper = value -> new BigDecimal(String.valueOf(value));
- }
- if (DOUBLE_TYPES.contains(field.type())) {
- mapper = value -> Double.valueOf(String.valueOf(value));
- }
- if (CHAR_TYPES.contains(field.type())) {
- mapper = value -> String.valueOf(value).charAt(0);
- }
- if (BOOLEAN_TYPES.contains(field.type())) {
- mapper = value -> {
- String lowered = String.valueOf(value).toLowerCase();
- if (lowered.equals("true") || lowered.equals("yes") || lowered.equals("1")) {
- return true;
- }
- return false;
- };
- }
- if (field.type().equals(LocalDate.class)) {
- mapper = value -> LocalDate.parse(String.valueOf(value));
- }
- if (field.type().equals(LocalDateTime.class)) {
- mapper = value -> LocalDateTime.parse(String.valueOf(value));
- }
- if (field.type().equals(ZonedDateTime.class)) {
- mapper = value -> ZonedDateTime.parse(String.valueOf(value));
- }
- if (field.type().isEnum()) {
- mapper = value -> toEnum(field.type(), String.valueOf(value));
- }
+ var mapper = TypeMappers.forType(field.type());
return new ReflectiveFieldAccessor(field, instance, mapper);
}
void set(Object value);
- @SuppressWarnings("unchecked")
- private static > T toEnum(Class> type, String value) {
- try {
- T[] enumConstants = (T[]) type.getEnumConstants();
- for (T constant : enumConstants) {
- if (value.equals(constant.name())) {
- return constant;
- }
- }
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- return null;
- }
}
diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlNodeStructureFactory.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/seraialize/XmlNodeStructureFactory.java
similarity index 91%
rename from xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlNodeStructureFactory.java
rename to xjx-serdes/src/main/java/io/jonasg/xjx/serdes/seraialize/XmlNodeStructureFactory.java
index ed59ac6..7b5ab18 100644
--- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlNodeStructureFactory.java
+++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/seraialize/XmlNodeStructureFactory.java
@@ -1,9 +1,10 @@
-package io.jonasg.xjx.serdes;
+package io.jonasg.xjx.serdes.seraialize;
-import io.jonasg.xjx.serdes.deserialize.Path;
+import io.jonasg.xjx.serdes.Section;
+import io.jonasg.xjx.serdes.Tag;
+import io.jonasg.xjx.serdes.Path;
import io.jonasg.xjx.serdes.reflector.InstanceField;
import io.jonasg.xjx.serdes.reflector.Reflector;
-import io.jonasg.xjx.serdes.seraialize.XmlNode;
public class XmlNodeStructureFactory {
diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlStringBuilder.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/seraialize/XmlStringBuilder.java
similarity index 97%
rename from xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlStringBuilder.java
rename to xjx-serdes/src/main/java/io/jonasg/xjx/serdes/seraialize/XmlStringBuilder.java
index 23e6bb0..e8e7d28 100644
--- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlStringBuilder.java
+++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/seraialize/XmlStringBuilder.java
@@ -1,4 +1,4 @@
-package io.jonasg.xjx.serdes;
+package io.jonasg.xjx.serdes.seraialize;
import io.jonasg.xjx.serdes.seraialize.XmlNode;
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 1df967c..7118cc7 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
@@ -2,16 +2,161 @@
import io.jonasg.xjx.serdes.Tag;
import io.jonasg.xjx.serdes.XjxSerdes;
-import org.assertj.core.api.Assertions;
import org.assertj.core.api.ThrowableAssert;
import org.junit.jupiter.api.Test;
+import java.time.LocalDate;
import java.util.List;
-import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class ListDeserializationTest {
+ @Test
+ void deserializeIntoListField_OfStringType() {
+ // given
+ String data = """
+
+
+
+ 2023-09-12
+ 2023-09-13
+ 2023-09-14
+ 2023-09-15
+
+
+ """;
+
+ // when
+ var dataHolder = new XjxSerdes().read(data, ListOfStrings.class);
+
+ // then
+ assertThat(dataHolder.strings).containsExactlyInAnyOrder(
+ "2023-09-12",
+ "2023-09-13",
+ "2023-09-14",
+ "2023-09-15"
+ );
+ }
+
+ static class ListOfStrings {
+ @Tag(path = "/Data/Strings", items = "String")
+ List strings;
+ }
+
+ @Test
+ void deserializeIntoListField_OfBooleanType() {
+ // given
+ String data = """
+
+
+
+ True
+ true
+ yes
+ YeS
+ 1
+
+
+ """;
+
+ // when
+ var weatherData = new XjxSerdes().read(data, ListOfBooleans.class);
+
+ // then
+ assertThat(weatherData.booleans).containsOnly(Boolean.TRUE);
+ }
+
+ static class ListOfBooleans {
+ @Tag(path = "/Data/Booleans", items = "Boolean")
+ List booleans;
+ }
+
+
+ @Test
+ void deserializeIntoListField_OfLongType() {
+ // given
+ String data = """
+
+
+
+ 123456789
+ -987654321
+ 0
+
+
+ """;
+
+ // when
+ var listOfLongs = new XjxSerdes().read(data, ListOfLongs.class);
+
+ // then
+ assertThat(listOfLongs.longs).containsExactly(123456789L, -987654321L, 0L);
+ }
+
+ static class ListOfLongs {
+ @Tag(path = "/Data/Longs", items = "Long")
+ List longs;
+ }
+
+ @Test
+ void deserializeIntoListField_OfDoubleType() {
+ // given
+ String data = """
+
+
+
+ 3.14
+ -2.5
+ 0.0
+
+
+ """;
+
+ // when
+ var listOfDoubles = new XjxSerdes().read(data, ListOfDoubles.class);
+
+ // then
+ assertThat(listOfDoubles.doubles).containsExactly(3.14, -2.5, 0.0);
+ }
+
+ static class ListOfDoubles {
+ @Tag(path = "/Data/Doubles", items = "Double")
+ List doubles;
+ }
+
+
+ @Test
+ void deserializeIntoListField_OfLocalDate() {
+ // given
+ String data = """
+
+
+
+ 2024-01-01
+ 2024-02-01
+ 2024-03-01
+
+
+ """;
+
+ // when
+ var listOfDates = new XjxSerdes().read(data, ListOfDates.class);
+
+ // then
+ assertThat(listOfDates.dates).containsExactly(
+ LocalDate.of(2024, 1, 1),
+ LocalDate.of(2024, 2, 1),
+ LocalDate.of(2024, 3, 1)
+ );
+ }
+
+ static class ListOfDates {
+ @Tag(path = "/Data/LocalDates", items = "LocalDate")
+ List dates;
+ }
+
@Test
void deserializeIntoListField_OfComplexType_ContainingTopLevelMapping() {
// given
@@ -58,11 +203,10 @@ public static class WeatherData {
public WeatherData() {
}
- @Tag(path = "/WeatherData/Forecasts")
+ @Tag(path = "/WeatherData/Forecasts", items = "Day")
List forecasts;
}
- @Tag(path = "/WeatherData/Forecasts/Day")
public static class Forecast {
public Forecast() {
}
@@ -75,42 +219,42 @@ public Forecast() {
void deserializeIntoListField_OfComplexType_ContainingComplexTypesWithCustomMapping() {
// given
String data = """
-
-
-
-
-
- 71
- °F
-
-
- 60
- °F
-
-
- 10
- %
-
- Partly Cloudy
-
-
-
- 78
- °F
-
-
- 62
- °F
-
-
- 12
- %
-
- Partly Cloudy
-
-
-
- """;
+
+
+
+
+
+ 71
+ °F
+
+
+ 60
+ °F
+
+
+ 10
+ %
+
+ Partly Cloudy
+
+
+
+ 78
+ °F
+
+
+ 62
+ °F
+
+
+ 12
+ %
+
+ Partly Cloudy
+
+
+
+ """;
// when
PrecipitationData precipitationData = new XjxSerdes().read(data, PrecipitationData.class);
@@ -125,34 +269,34 @@ void deserializeIntoListField_OfComplexType_ContainingComplexTypesWithCustomMapp
void deserializeIntoListField_EvenIfNoneOfTheInnerMappedFieldsOfComplexTypeCanBeMapped() {
// given
String data = """
-
-
-
-
-
- 71
- °F
-
-
- 60
- °F
-
- Partly Cloudy
-
-
-
- 78
- °F
-
-
- 62
- °F
-
- Partly Cloudy
-
-
-
- """;
+
+
+
+
+
+ 71
+ °F
+
+
+ 60
+ °F
+
+ Partly Cloudy
+
+
+
+ 78
+ °F
+
+
+ 62
+ °F
+
+ Partly Cloudy
+
+
+
+ """;
// when
PrecipitationData precipitationData = new XjxSerdes().read(data, PrecipitationData.class);
@@ -165,11 +309,11 @@ void deserializeIntoListField_EvenIfNoneOfTheInnerMappedFieldsOfComplexTypeCanBe
void listsMappedOntoSelfClosingTag_containsEmptyList() {
// given
String data = """
-
-
-
-
- """;
+
+
+
+
+ """;
// when
PrecipitationData precipitationData = new XjxSerdes().read(data, PrecipitationData.class);
@@ -183,12 +327,11 @@ static class PrecipitationData {
public PrecipitationData() {
}
- @Tag(path = "/WeatherData/Forecasts")
+ @Tag(path = "/WeatherData/Forecasts", items = "Day")
List precipitations;
}
- @Tag(path = "/WeatherData/Forecasts/Day")
static class Precipitation {
public Precipitation() {
@@ -208,7 +351,7 @@ public PrecipitationValue() {
}
@Test
- void informUserThatAList_itsGenericType_shouldBeAnnotatedWithTag() {
+ void informUserThatAMappedListField_shouldHaveAFilledInItemsParameter() {
// given
String data = """
@@ -245,9 +388,10 @@ void informUserThatAList_itsGenericType_shouldBeAnnotatedWithTag() {
// then
assertThatThrownBy(when)
- .hasMessage("Generics of type List require @Tag pointing to mapped XML path (ForecastWithMissingTag)");
+ .hasMessage("Field (ForecastWithMissingTag) requires @Tag to have items parameter describing the tag name of a single repeated tag");
}
+ @SuppressWarnings("unused")
public static class WeatherDataWithMissingTag {
public WeatherDataWithMissingTag() {
}
@@ -256,6 +400,7 @@ public WeatherDataWithMissingTag() {
List forecasts;
}
+ @SuppressWarnings("unused")
public static class ForecastWithMissingTag {
public ForecastWithMissingTag() {
}
@@ -310,11 +455,10 @@ public static class WeatherDataRelativeMapping {
public WeatherDataRelativeMapping() {
}
- @Tag(path = "/WeatherData/Forecasts")
+ @Tag(path = "/WeatherData/Forecasts", items = "Day")
List forecasts;
}
- @Tag(path = "/WeatherData/Forecasts/Day")
public static class ForecastRelativeMapping {
public ForecastRelativeMapping() {
}
@@ -371,11 +515,10 @@ public static class WeatherDataRelativeAndAbsoluteMapping {
public WeatherDataRelativeAndAbsoluteMapping() {
}
- @Tag(path = "/WeatherData/Forecasts")
+ @Tag(path = "/WeatherData/Forecasts", items = "Day")
List forecasts;
}
- @Tag(path = "/WeatherData/Forecasts/Day")
public static class ForecastRelativeAndAbsoluteMapping {
public ForecastRelativeAndAbsoluteMapping() {
}
@@ -430,11 +573,10 @@ static class Gpx {
public Gpx() {
}
- @Tag(path = "/gpx")
+ @Tag(path = "/gpx", items = "wpt")
List wayPoints;
}
- @Tag(path = "/gpx/wpt")
static class Wpt {
public Wpt() {
}
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 229852b..f267170 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
@@ -12,6 +12,40 @@
import static org.assertj.core.api.Assertions.assertThat;
public class SetDeserializationTest {
+
+ @Test
+ void deserializeIntoListField_OfStringType() {
+ // given
+ String data = """
+
+
+
+ 2023-09-12
+ 2023-09-13
+ 2023-09-14
+ 2023-09-15
+
+
+ """;
+
+ // when
+ var dataHolder = new XjxSerdes().read(data, ListOfStrings.class);
+
+ // then
+ assertThat(dataHolder.strings).containsExactlyInAnyOrder(
+ "2023-09-12",
+ "2023-09-13",
+ "2023-09-14",
+ "2023-09-15"
+ );
+ }
+
+ static class ListOfStrings {
+ @Tag(path = "/Data/Strings", items = "String")
+ Set strings;
+ }
+
+
@Test
void deserializeIntoSetField_OfComplexType_ContainingTopLevelMapping() {
// given
@@ -57,14 +91,13 @@ void deserializeIntoSetField_OfComplexType_ContainingTopLevelMapping() {
}
public static class WeatherData {
- @Tag(path = "/WeatherData/Forecasts")
+ @Tag(path = "/WeatherData/Forecasts", items = "Day")
Set forecasts;
public WeatherData() {
}
}
- @Tag(path = "/WeatherData/Forecasts/Day")
public static class Forecast {
@Tag(path = "/WeatherData/Forecasts/Day/High/Value")
@@ -150,12 +183,11 @@ static class PrecipitationData {
public PrecipitationData() {
}
- @Tag(path = "/WeatherData/Forecasts")
+ @Tag(path = "/WeatherData/Forecasts", items = "Day")
Set precipitations;
}
- @Tag(path = "/WeatherData/Forecasts/Day")
static class Precipitation {
PrecipitationValue precipitationValue;
@@ -212,7 +244,7 @@ public int hashCode() {
}
@Test
- void informUserThatASet_itsGenericType_shouldBeAnnotatedWithTag() {
+ void informUserThatFieldOfTypeSet_shouldHaveItemsFilledIn() {
// given
String data = """
@@ -249,7 +281,7 @@ void informUserThatASet_itsGenericType_shouldBeAnnotatedWithTag() {
// then
Assertions.assertThatThrownBy(when)
- .hasMessage("Generics of type Set require @Tag pointing to mapped XML path (ForecastWithMissingTag)");
+ .hasMessage("Field (ForecastWithMissingTag) requires @Tag to have items parameter describing the tag name of a single repeated tag");
}
public static class WeatherDataWithMissingTag {
@@ -314,7 +346,7 @@ public static class WeatherDataRelativeMapping {
public WeatherDataRelativeMapping() {
}
- @Tag(path = "/WeatherData/Forecasts")
+ @Tag(path = "/WeatherData/Forecasts", items = "Day")
Set forecasts;
}
@@ -373,7 +405,7 @@ public static class WeatherDataRelativeAndAbsoluteMapping {
public WeatherDataRelativeAndAbsoluteMapping() {
}
- @Tag(path = "/WeatherData/Forecasts")
+ @Tag(path = "/WeatherData/Forecasts", items = "Day")
Set forecasts;
}
@@ -430,7 +462,7 @@ static class Gpx {
public Gpx() {
}
- @Tag(path = "/gpx")
+ @Tag(path = "/gpx", items = "wpt")
Set wayPoints;
}
@@ -445,5 +477,4 @@ public Wpt() {
@Tag(path = "time")
String time;
}
-
}
diff --git a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/seraialize/GeneralSerializationTest.java b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/serialize/GeneralSerializationTest.java
similarity index 98%
rename from xjx-serdes/src/test/java/io/jonasg/xjx/serdes/seraialize/GeneralSerializationTest.java
rename to xjx-serdes/src/test/java/io/jonasg/xjx/serdes/serialize/GeneralSerializationTest.java
index e806283..4476378 100644
--- a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/seraialize/GeneralSerializationTest.java
+++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/serialize/GeneralSerializationTest.java
@@ -1,4 +1,4 @@
-package io.jonasg.xjx.serdes.seraialize;
+package io.jonasg.xjx.serdes.serialize;
import io.jonasg.xjx.serdes.Tag;
import io.jonasg.xjx.serdes.XjxSerdes;
diff --git a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/seraialize/TagAttributeSerializationTest.java b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/serialize/TagAttributeSerializationTest.java
similarity index 99%
rename from xjx-serdes/src/test/java/io/jonasg/xjx/serdes/seraialize/TagAttributeSerializationTest.java
rename to xjx-serdes/src/test/java/io/jonasg/xjx/serdes/serialize/TagAttributeSerializationTest.java
index 9f1249b..35051cb 100644
--- a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/seraialize/TagAttributeSerializationTest.java
+++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/serialize/TagAttributeSerializationTest.java
@@ -1,4 +1,4 @@
-package io.jonasg.xjx.serdes.seraialize;
+package io.jonasg.xjx.serdes.serialize;
import io.jonasg.xjx.serdes.Tag;
import io.jonasg.xjx.serdes.XjxSerdes;