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;