From 50ff43c2aea8ed9a37254f4042aaa672c9a5cc53 Mon Sep 17 00:00:00 2001 From: JonasG Date: Tue, 9 Jan 2024 20:42:55 +0100 Subject: [PATCH] feat: add preliminary serialization functionality --- .gitignore | 4 + README.md | 39 +- pom.xml | 80 ++-- xjx-sax/pom.xml | 2 +- .../main/java/io/jonasg/xjx/Attributes.java | 7 +- xjx-serdes/pom.xml | 2 +- .../java/io/jonasg/xjx/serdes/Section.java | 35 ++ .../java/io/jonasg/xjx/serdes/XjxSerdes.java | 21 +- .../xjx/serdes/XmlNodeStructureFactory.java | 51 +++ .../jonasg/xjx/serdes/XmlStringBuilder.java | 55 +++ .../jonasg/xjx/serdes/deserialize/Path.java | 33 +- .../deserialize/PathWriterIndexFactory.java | 12 +- .../xjx/serdes/reflector/FieldReflector.java | 2 +- .../xjx/serdes/reflector/InstanceField.java | 45 +++ .../serdes/reflector/InstanceReflector.java | 10 + .../xjx/serdes/reflector/Reflector.java | 2 +- .../jonasg/xjx/serdes/seraialize/XmlNode.java | 110 ++++++ ...va => DeserializationFieldAccessTest.java} | 2 +- ... DeserializationInstanceCreationTest.java} | 2 +- ...t.java => GeneralDeserializationTest.java} | 2 +- .../seraialize/GeneralSerializationTest.java | 100 +++++ .../TagAttributeSerializationTest.java | 370 ++++++++++++++++++ 22 files changed, 933 insertions(+), 53 deletions(-) create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Section.java create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlNodeStructureFactory.java create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlStringBuilder.java create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/InstanceField.java create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/seraialize/XmlNode.java rename xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/{FieldAccessTest.java => DeserializationFieldAccessTest.java} (98%) rename xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/{InstanceCreationTest.java => DeserializationInstanceCreationTest.java} (97%) rename xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/{GeneralMappingTest.java => GeneralDeserializationTest.java} (99%) create mode 100644 xjx-serdes/src/test/java/io/jonasg/xjx/serdes/seraialize/GeneralSerializationTest.java create mode 100644 xjx-serdes/src/test/java/io/jonasg/xjx/serdes/seraialize/TagAttributeSerializationTest.java diff --git a/.gitignore b/.gitignore index bbe41e3..ad52d6f 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,7 @@ build/ ### Mac OS ### .DS_Store + +### Jreleaser +out/ +trace.log diff --git a/README.md b/README.md index a2392c3..41e9995 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Xjx -Streamlined XML serdes library: No Dependencies, Just Simplicity +XML serializing and deserializing (serdes) library: No Dependencies, Just Simplicity # 🤔 Why The "why" behind Xjx is rooted in the necessity for a minimalist, actively maintained XML-to-Java and vice versa library. @@ -12,12 +12,12 @@ Xjx exists out of two modules: # 🔑 Key Features - Explicitly map fields to specific tags using `@Tag` - Select specific tags using an **XPath** like expression `@Tag(path = "/WeatherData/Location/City)` -- Out of the box support for most common data types +- Out-of-the-box support for most common data types - Explicit deserialization of values using `@ValueDeserialization` # ✨ xjx-serdes -Contains the XML serializer (TODO) and deserialization code. +Contains the XML serializer and deserializer. ## ⚙️ Installation @@ -25,7 +25,7 @@ Contains the XML serializer (TODO) and deserialization code. io.jonasg xjx-serdes - 0.1.0 + 0.2.0 ``` @@ -79,7 +79,10 @@ String document = """ """; -var weatherData = new XjxSerdes().read(document, WeatherData.class); +var xjx = new XjxSerdes(); +WeatherData weatherData = xjx.read(document, WeatherData.class); + +String xmlDocument = xjx.write(weatherData); ``` ## General deserialization rules Deserialization is guided by the use of the `@Tag` annotation. Fields annotated with `@Tag` are candidates for deserialization, while unannotated fields are ignored. @@ -235,3 +238,29 @@ Map.of("CurrentConditions", Map.of("Temperature", Map.of("Value", "75", "Unit", "°F")))); ``` +## General serialization rules + +Fields annotated with `@Tag` are considered for serialization, while unannotated fields are ignored. +### Path Expressions +Fields are serialized based on the path property specified in the @Tag annotation. +The path property uses an XPath-like expression to determine the location of the field within the XML document. + +```java +class WeatherData { + @Tag(path = "/WeatherData/Location/Country") + private final String country; + + @Tag(path = "/WeatherData/Location/City/Name") + private final String city; + + // Constructor and other methods are omitted for brevity +} +``` + +In this example, the country field is serialized to within the specified path, +and the city field is serialized to . + +## Null Fields + +Null fields are serialized as self-closing tags by default. +If a field is null, the corresponding XML tag is included, but the tag content is empty. diff --git a/pom.xml b/pom.xml index c247e10..5f19086 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.jonasg xjx - 0.1.1-SNAPSHOT + 0.2.0 pom xjx-sax @@ -142,36 +142,62 @@ - - org.jreleaser - jreleaser-maven-plugin - - - - ALWAYS - true - - - - - - ALWAYS - https://s01.oss.sonatype.org/service/local - https://s01.oss.sonatype.org/content/repositories/snapshots/ - true - true - target/staging-deploy - - - - - - - + + release + + + + org.jreleaser + jreleaser-maven-plugin + false + + + + Xjx - Lightweight XML Library for Java + + https://github.com/jonasgeiregat/xjx + + APACHE-2.0 + Jonas Geiregat + 2023 Jonas Geiregat + + + + + ALWAYS + conventional-commits + + + + + ALWAYS + true + + + + + + ALWAYS + https://s01.oss.sonatype.org/service/local + https://s01.oss.sonatype.org/content/repositories/snapshots/ + false + false + target/staging-deploy + + + + + + + + + + + publication diff --git a/xjx-sax/pom.xml b/xjx-sax/pom.xml index 858697c..2ae09e6 100644 --- a/xjx-sax/pom.xml +++ b/xjx-sax/pom.xml @@ -4,7 +4,7 @@ io.jonasg xjx - 0.1.1-SNAPSHOT + 0.2.0 xjx-sax diff --git a/xjx-sax/src/main/java/io/jonasg/xjx/Attributes.java b/xjx-sax/src/main/java/io/jonasg/xjx/Attributes.java index a51c406..2823d5e 100644 --- a/xjx-sax/src/main/java/io/jonasg/xjx/Attributes.java +++ b/xjx-sax/src/main/java/io/jonasg/xjx/Attributes.java @@ -1,12 +1,13 @@ package io.jonasg.xjx; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.StringJoiner; import java.util.stream.Stream; public class Attributes { - private final Map attributes = new HashMap<>(); + private final Map attributes = new LinkedHashMap<>(); public Attributes(String... values) { int length = values.length; @@ -40,6 +41,10 @@ public Stream stream() { .map(e -> new Attribute(e.getKey(), e.getValue())); } + public boolean isEmpty() { + return attributes.isEmpty(); + } + public record Attribute(String name, String value) {} diff --git a/xjx-serdes/pom.xml b/xjx-serdes/pom.xml index 75245f9..ff53b98 100644 --- a/xjx-serdes/pom.xml +++ b/xjx-serdes/pom.xml @@ -4,7 +4,7 @@ io.jonasg xjx - 0.1.1-SNAPSHOT + 0.2.0 xjx-serdes 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 new file mode 100644 index 0000000..42639cd --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Section.java @@ -0,0 +1,35 @@ +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(); + } +} 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 57b8084..4b95216 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 @@ -21,16 +21,20 @@ public class XjxSerdes { private final PathWriterIndexFactory pathWriterIndexFactory; - private XjxSerdes(SaxParser saxParser, PathWriterIndexFactory pathWriterIndexFactory) { + private final XmlNodeStructureFactory xmlNodeStructureFactory = new XmlNodeStructureFactory(); + private final XmlStringBuilder xmlStringBuilder; + + private XjxSerdes(SaxParser saxParser, PathWriterIndexFactory pathWriterIndexFactory, XmlStringBuilder xmlStringBuilder) { this.saxParser = saxParser; this.pathWriterIndexFactory = pathWriterIndexFactory; + this.xmlStringBuilder = xmlStringBuilder; } /** * Constructs an XjxSerdes instance with default configurations. */ public XjxSerdes() { - this(new SaxParser(), new PathWriterIndexFactory()); + this(new SaxParser(), new PathWriterIndexFactory(), new XmlStringBuilder()); } /** @@ -92,4 +96,17 @@ public Map read(Reader data, MapOf mapOf) { } throw new XjxDeserializationException("Maps only support String as key"); } + + /** + * Writes an object to an XML document. + * + * @param data The object to serialize to XML. + * @param The generic type of the object. + * @return The XML representation of the object. + */ + public String write(T data) { + var nodes = xmlNodeStructureFactory.build(data); + return xmlStringBuilder.build(nodes); + } + } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlNodeStructureFactory.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlNodeStructureFactory.java new file mode 100644 index 0000000..ed59ac6 --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlNodeStructureFactory.java @@ -0,0 +1,51 @@ +package io.jonasg.xjx.serdes; + +import io.jonasg.xjx.serdes.deserialize.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 { + + public XmlNode build(T data) { + return getXmlNode(Path.parse("/"), data, null); + } + + private XmlNode getXmlNode(Path parentPath, T data, XmlNode node) { + if (data != null) { + for (InstanceField field : Reflector.reflect(data) + .fields(f -> f.hasAnnotation(Tag.class))) { + node = buildNodeForField(field, parentPath, node); + } + } + return node; + } + + private XmlNode buildNodeForField(InstanceField field, Path parentPath, XmlNode rootNode) { + var tag = field.getAnnotation(Tag.class); + var path = parentPath.append(Path.parse(tag.path())); + if (rootNode == null) { + rootNode = new XmlNode(path.getRoot()); + } + var node = rootNode; + for (int i = 1; i < path.size(); i++) { + Section section = path.getSection(i); + if (section.isLeaf()) { + handleLeafNode(field, section, tag, node); + } else { + node = node.addNode(section.name()); + } + } + return getXmlNode(path, field.getValue(), rootNode); + } + + private static void handleLeafNode(InstanceField field, Section section, Tag tag, XmlNode node) { + if (!tag.attribute().isEmpty()) { + node.addNode(section.name()) + .addAttribute(tag.attribute(), field.getValue()); + } else { + node.addValueNode(section.name(), field.getValue()); + } + } + +} diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlStringBuilder.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlStringBuilder.java new file mode 100644 index 0000000..23e6bb0 --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XmlStringBuilder.java @@ -0,0 +1,55 @@ +package io.jonasg.xjx.serdes; + +import io.jonasg.xjx.serdes.seraialize.XmlNode; + +import java.util.List; + +public class XmlStringBuilder { + + public String build(XmlNode nodes) { + var sb = new StringBuilder(); + sb.append("<").append(nodes.name()).append(">\n"); + buildNodes(nodes.children(), sb); + sb.append("\n"); + return sb.toString(); + } + + private void buildNodes(List nodes, StringBuilder sb) { + int indentationLevel = 1; + buildNodes(nodes, sb, indentationLevel); + } + + private void buildNodes(List nodes, StringBuilder sb, int indentationLevel) { + String indentation = " ".repeat(indentationLevel); + + nodes.forEach(node -> { + sb.append(indentation) + .append("<").append(node.name()); + + if (node.hasAttributes()) { + node.attributes().stream().forEach(attribute -> + sb.append(" ") + .append(attribute.name()) + .append("=\"") + .append(attribute.value()) + .append("\"")); + } + + if (node.hasChildren() || node.containsAValue()) { + sb.append(">"); + if (node.containsAValue()) { + sb.append(node.value()); + } + if (node.hasChildren()) { + sb.append("\n"); + buildNodes(node.children(), sb, indentationLevel + 1); + sb.append(indentation); + } + sb.append("\n"); + } else { + sb.append("/>\n"); + } + }); + } + +} 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/deserialize/Path.java index d4d9b00..a431d96 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/Path.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/Path.java @@ -1,10 +1,13 @@ package io.jonasg.xjx.serdes.deserialize; +import io.jonasg.xjx.serdes.Section; + import java.util.Arrays; +import java.util.Iterator; import java.util.LinkedList; import java.util.Objects; -public class Path { +public class Path implements Iterable
{ private final LinkedList sections = new LinkedList<>(); @@ -58,6 +61,30 @@ public Path pop() { } } + public String getRoot() { + return sections.getFirst(); + } + + public int size() { + return sections.size(); + } + + public boolean isRoot() { + return sections.size() == 1; + } + + public Section getSection(int position) { + return new Section(this.sections.get(position), position == this.sections.size() - 1); + } + + @Override + public Iterator
iterator() { + int size = sections.size(); + return sections.stream() + .map(s -> new Section(s, sections.indexOf(s) == size - 1)) + .iterator(); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -80,8 +107,4 @@ public int hashCode() { public String toString() { return "/" + String.join("/", sections) + (attribute == null ? "" : "[" + attribute + "]"); } - - public boolean isRoot() { - return sections.size() == 1; - } } 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 19f22e7..dc2042a 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 @@ -95,7 +95,7 @@ private static void indexMapAsRootType(FieldReflector field, Map index, Path path, Supplier parent) { - if (field.isAnnotatedWith(Tag.class)) { + if (field.hasAnnotation(Tag.class)) { doIndexComplexType(field, index, path, parent); } else { searchFieldsRecursivelyForTag(field) @@ -112,7 +112,7 @@ private void indexComplexType(FieldReflector field, Map index, } private void doIndexComplexType(FieldReflector field, Map index, Path path, Supplier parent) { - if (field.isAnnotatedWith(ValueDeserialization.class)) { + if (field.hasAnnotation(ValueDeserialization.class)) { index.put(getPathForField(field, path), PathWriter.valueInitializer((value) -> { value = ValueDeserializationHandler.getInstance().handle(field.rawField(), (String) value) .orElse(value); @@ -133,7 +133,7 @@ private void doIndexComplexType(FieldReflector field, Map inde } private Optional searchFieldsRecursivelyForTag(FieldReflector field) { - if (field.isAnnotatedWith(Tag.class)) { + if (field.hasAnnotation(Tag.class)) { return Optional.of(new TagPath(field.getAnnotation(Tag.class), field)); } @@ -143,7 +143,7 @@ private Optional searchFieldsRecursivelyForTag(FieldReflector field) { for (Field subField : fields) { FieldReflector subFieldReflector = new FieldReflector(subField); if (BASIC_TYPES.contains(subField.getType())) { - if (subFieldReflector.isAnnotatedWith(Tag.class)) { + if (subFieldReflector.hasAnnotation(Tag.class)) { return Optional.of(new TagPath(subFieldReflector.getAnnotation(Tag.class), subFieldReflector)); } return Optional.empty(); @@ -155,7 +155,7 @@ private Optional searchFieldsRecursivelyForTag(FieldReflector field) { private void indexEnumType(FieldReflector field, Map index, Path path, Supplier parent) { index.put(getPathForField(field, path), PathWriter.valueInitializer((value) -> { - if (field.isAnnotatedWith(ValueDeserialization.class)) { + if (field.hasAnnotation(ValueDeserialization.class)) { value = ValueDeserializationHandler.getInstance().handle(field.rawField(), (String) value) .orElse(value); } @@ -164,7 +164,7 @@ private void indexEnumType(FieldReflector field, Map index, Pa } private void indexSimpleType(FieldReflector field, Map index, Path path, Supplier parent) { - if (field.isAnnotatedWith(Tag.class)) { + if (field.hasAnnotation(Tag.class)) { index.put(getPathForField(field, path), PathWriter.valueInitializer((value) -> { if (value instanceof String) { value = ValueDeserializationHandler.getInstance().handle(field.rawField(), (String) value) diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/FieldReflector.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/FieldReflector.java index 64d40e0..c1542a3 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/FieldReflector.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/FieldReflector.java @@ -47,7 +47,7 @@ public T getAnnotation(Class clazz) { return field.getAnnotation(clazz); } - public boolean isAnnotatedWith(Class annotation) { + public boolean hasAnnotation(Class annotation) { return field.getAnnotation(annotation) != null; } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/InstanceField.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/InstanceField.java new file mode 100644 index 0000000..1a8437d --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/InstanceField.java @@ -0,0 +1,45 @@ +package io.jonasg.xjx.serdes.reflector; + +import java.lang.annotation.Annotation; +import java.util.StringJoiner; + +public class InstanceField { + private final FieldReflector fieldReflector; + private final Object instance; + + public InstanceField(FieldReflector fieldReflector, T instance) { + this.fieldReflector = fieldReflector; + this.instance = instance; + } + + public boolean hasAnnotation(Class annotation) { + return fieldReflector.hasAnnotation(annotation); + } + + public T getAnnotation(Class type) { + return fieldReflector.getAnnotation(type); + } + + public Object getValue() { + try { + fieldReflector.rawField().setAccessible(true); + Object value = fieldReflector.rawField().get(instance); + fieldReflector.rawField().setAccessible(false); + return value; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public Class type() { + return fieldReflector.type(); + } + + @Override + public String toString() { + return new StringJoiner(", ", InstanceField.class.getSimpleName() + "[", "]") + .add("fieldReflector=" + fieldReflector) + .add("instance=" + instance) + .toString(); + } +} diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/InstanceReflector.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/InstanceReflector.java index fcbf98a..a81cea6 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/InstanceReflector.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/InstanceReflector.java @@ -3,6 +3,8 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; public class InstanceReflector { @@ -52,6 +54,14 @@ public void setField(String fieldName, Object value) { .ifPresent(f -> f.set(instance, value)); } + public List fields(Predicate predicate) { + return typeReflector.fields() + .stream() + .map(f -> new InstanceField(f, instance)) + .filter(predicate) + .toList(); + } + public T instance() { return instance; } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/Reflector.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/Reflector.java index 64d2c45..f9b249d 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/Reflector.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/Reflector.java @@ -2,7 +2,7 @@ public class Reflector { - public InstanceReflector reflect(T instance) { + public static InstanceReflector reflect(T instance) { return new InstanceReflector<>(instance); } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/seraialize/XmlNode.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/seraialize/XmlNode.java new file mode 100644 index 0000000..d7c11dc --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/seraialize/XmlNode.java @@ -0,0 +1,110 @@ +package io.jonasg.xjx.serdes.seraialize; + +import io.jonasg.xjx.Attributes; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class XmlNode { + private final String name; + private Object value; + private final List children; + private final Attributes attributes; + + public XmlNode(String name, Object value, List children, Attributes attributes) { + this.name = name; + this.value = value; + this.children = children; + this.attributes = attributes; + } + + public XmlNode(String name) { + this(name, null, new ArrayList<>(), new Attributes()); + } + + public XmlNode(String name, Object value) { + this(name, value, null, new Attributes()); + } + + public void addValueNode(String name, Object value) { + this.children.stream() + .filter(n -> Objects.equals(n.name, name)) + .findFirst() + .map(n -> n.value = value) + .orElseGet(() -> { + var node = new XmlNode(name, value); + this.children.add(node); + return node; + }); + } + + public XmlNode addNode(String name) { + return this.children.stream() + .filter(n -> Objects.equals(n.name, name)) + .findFirst() + .orElseGet(() -> { + var node = new XmlNode(name); + this.children.add(node); + return node; + }); + } + + public void addAttribute(String attribute, Object value) { + this.attributes.add(attribute, String.valueOf(value)); + } + + public boolean hasChildren() { + return this.children != null && !this.children.isEmpty(); + } + + public boolean containsAValue() { + return value != null; + } + + public boolean hasAttributes() { + return !this.attributes.isEmpty(); + } + + public String name() { + return name; + } + + public Object value() { + return value; + } + + public List children() { + return children; + } + + public Attributes attributes() { + return attributes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (XmlNode) obj; + return Objects.equals(this.name, that.name) && + Objects.equals(this.value, that.value) && + Objects.equals(this.children, that.children) && + Objects.equals(this.attributes, that.attributes); + } + + @Override + public int hashCode() { + return Objects.hash(name, value, children, attributes); + } + + @Override + public String toString() { + return "XmlNode[" + + "name=" + name + ", " + + "value=" + value + ", " + + "children=" + children + ", " + + "attributes=" + attributes + ']'; + } + +} diff --git a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/FieldAccessTest.java b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/DeserializationFieldAccessTest.java similarity index 98% rename from xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/FieldAccessTest.java rename to xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/DeserializationFieldAccessTest.java index d2f6e4a..fe1c8f9 100644 --- a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/FieldAccessTest.java +++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/DeserializationFieldAccessTest.java @@ -5,7 +5,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -public class FieldAccessTest { +public class DeserializationFieldAccessTest { @Test void accessThroughPublicField() { diff --git a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/InstanceCreationTest.java b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/DeserializationInstanceCreationTest.java similarity index 97% rename from xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/InstanceCreationTest.java rename to xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/DeserializationInstanceCreationTest.java index c26c274..81c4cae 100644 --- a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/InstanceCreationTest.java +++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/DeserializationInstanceCreationTest.java @@ -5,7 +5,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -public class InstanceCreationTest { +public class DeserializationInstanceCreationTest { @Test void instantiateUsingDefaultConstructor() { diff --git a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralMappingTest.java b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralDeserializationTest.java similarity index 99% rename from xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralMappingTest.java rename to xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralDeserializationTest.java index c08c68f..0ec0770 100644 --- a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralMappingTest.java +++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralDeserializationTest.java @@ -8,7 +8,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class GeneralMappingTest { +public class GeneralDeserializationTest { @Test void ignoreUnmappedFieldsAndLeaveThemUninitialized() { 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/seraialize/GeneralSerializationTest.java new file mode 100644 index 0000000..e806283 --- /dev/null +++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/seraialize/GeneralSerializationTest.java @@ -0,0 +1,100 @@ +package io.jonasg.xjx.serdes.seraialize; + +import io.jonasg.xjx.serdes.Tag; +import io.jonasg.xjx.serdes.XjxSerdes; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class GeneralSerializationTest { + @Test + void serializeNestedTags() { + // given + var weatherData = new WeatherData("USA", "New York"); + + // when + String xml = new XjxSerdes().write(weatherData); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + USA + + New York + + + + """); + } + + @Test + void serializeNullFieldsToSelfClosingTag() { + // given + var weatherData = new WeatherData(null, "New York"); + + // when + String xml = new XjxSerdes().write(weatherData); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + + New York + + + + """); + } + + + @Test + void serializeNullFieldsToSelfClosingTagContainingNonNullAttribute() { + // given + var weatherData = new WeatherDataWithAttribute(null, "USA"); + + // when + String xml = new XjxSerdes().write(weatherData); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + + + """); + } + + @SuppressWarnings("unused") + static class WeatherData { + + @Tag(path = "/WeatherData/Location/Country") + private final String country; + + @Tag(path = "/WeatherData/Location/City/Name") + private final String city; + + public WeatherData(String country, String city) { + this.country = country; + this.city = city; + } + } + + + @SuppressWarnings("unused") + static class WeatherDataWithAttribute { + + @Tag(path = "/WeatherData/Location/Country") + private final String country; + + @Tag(path = "/WeatherData/Location/Country", attribute = "code") + private final String code; + + public WeatherDataWithAttribute(String country, String code) { + this.country = country; + this.code = code; + } + } +} 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/seraialize/TagAttributeSerializationTest.java new file mode 100644 index 0000000..9f1249b --- /dev/null +++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/seraialize/TagAttributeSerializationTest.java @@ -0,0 +1,370 @@ +package io.jonasg.xjx.serdes.seraialize; + +import io.jonasg.xjx.serdes.Tag; +import io.jonasg.xjx.serdes.XjxSerdes; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +public class TagAttributeSerializationTest { + + @Test + void serialize_TagContainingAValueAndMultipleAttributes() { + // given + var dataHolder = new TagContainingValueAndAttributes("11", "A", "B"); + + // when + String xml = new XjxSerdes().write(dataHolder); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + 11 + + """); + } + + @Test + void serialize_ToEmptyTag_ContainingMultipleAttributes() { + // given + var dataType = new TagContainingMultipleAttributesAndNoValue("A", "B"); + + // when + String xml = new XjxSerdes().write(dataType); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + @Test + void serialize_IntegerField() { + // given + IntegerData dataTypes = new IntegerData(11); + + // when + String xml = new XjxSerdes().write(dataTypes); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + @Test + void serialize_LongField() { + // given + LongData dataTypes = new LongData(11L); + + // when + String xml = new XjxSerdes().write(dataTypes); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + @Test + void serialize_primitiveLongField() { + // given + PrimitiveLongData dataTypes = new PrimitiveLongData(11L); + + // when + String xml = new XjxSerdes().write(dataTypes); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + @Test + void serialize_BigDecimalField() { + // given + BigDecimalData dataTypes = new BigDecimalData(BigDecimal.valueOf(11)); + + // when + String xml = new XjxSerdes().write(dataTypes); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + @Test + void serialize_DoubleField() { + // given + DoubleData dataTypes = new DoubleData(11.0); + + // when + String xml = new XjxSerdes().write(dataTypes); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + @Test + void serialize_primitiveDoubleField() { + // given + PrimitiveDoubleData dataTypes = new PrimitiveDoubleData(11); + + // when + String xml = new XjxSerdes().write(dataTypes); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + @Test + void serialize_multiCharString_toPrimitiveCharField() { + // given + MultipleCharactersData dataTypes = new MultipleCharactersData('A'); + + // when + String xml = new XjxSerdes().write(dataTypes); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + @Test + void serialize_primitiveCharField() { + // given + PrimitiveCharData dataTypes = new PrimitiveCharData('A'); + + // when + String xml = new XjxSerdes().write(dataTypes); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + @Test + void serialize_CharacterField() { + // given + CharacterData dataTypes = new CharacterData('A'); + + // when + String xml = new XjxSerdes().write(dataTypes); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + @Test + void serialize_booleanFields() { + // given + BooleanData dataTypes = new BooleanData(true, true, false, false); + + // when + String xml = new XjxSerdes().write(dataTypes); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + + """); + } + + @Test + void serialize_TagMappedUsingRelativePath() { + // given + ParentHolder parentHolder = new ParentHolder(); + parentHolder.nestedField = new NestedField.StringData("11"); + + // when + String xml = new XjxSerdes().write(parentHolder); + + // then + Assertions.assertThat(xml).isEqualTo(""" + + + + """); + } + + public static class TagContainingValueAndAttributes { + public TagContainingValueAndAttributes(String tagValue, String attributeA, String attributeB) { + this.attributeA = attributeA; + this.attributeB = attributeB; + this.tagValue = tagValue; + } + + @Tag(path = "/DataTypes/String", attribute = "attrA") + String attributeA; + + @Tag(path = "/DataTypes/String", attribute = "attrB") + String attributeB; + + @Tag(path = "/DataTypes/String") + String tagValue; + } + + public static class TagContainingMultipleAttributesAndNoValue { + + public TagContainingMultipleAttributesAndNoValue(String attrA, String attrB) { + this.attrA = attrA; + this.attrB = attrB; + } + + @Tag(path = "/DataTypes/String", attribute = "attrA") + String attrA; + + @Tag(path = "/DataTypes/String", attribute = "attrB") + String attrB; + } + + public static class IntegerData { + public IntegerData(Integer value) { + this.Integer = value; + } + + @Tag(path = "/DataTypes/Integer", attribute = "value") + Integer Integer; + } + + public static class LongData { + public LongData(Long value) { + this.Long = value; + } + + @Tag(path = "/DataTypes/Long", attribute = "value") + Long Long; + } + + public static class PrimitiveLongData { + public PrimitiveLongData(long value) { + this.primitiveLong = value; + } + + @Tag(path = "/DataTypes/primitiveLong", attribute = "value") + long primitiveLong; + } + + public static class BigDecimalData { + public BigDecimalData(BigDecimal value) { + this.BigDecimal = value; + } + + @Tag(path = "/DataTypes/BigDecimal", attribute = "value") + BigDecimal BigDecimal; + } + + public static class DoubleData { + public DoubleData(Double value) { + this.Double = value; + } + + @Tag(path = "/DataTypes/Double", attribute = "value") + Double Double; + } + + public static class PrimitiveDoubleData { + public PrimitiveDoubleData(double value) { + this.primitiveDouble = value; + } + + @Tag(path = "/DataTypes/primitiveDouble", attribute = "value") + double primitiveDouble; + } + + public static class MultipleCharactersData { + public MultipleCharactersData(char value) { + this.multipleCharacters = value; + } + + @Tag(path = "/DataTypes/multipleCharacters", attribute = "value") + char multipleCharacters; + } + + public static class PrimitiveCharData { + public PrimitiveCharData(char value) { + this.primitiveChar = value; + } + + @Tag(path = "/DataTypes/primitiveChar", attribute = "value") + char primitiveChar; + } + + public static class CharacterData { + public CharacterData(char value) { + this.Character = value; + } + + @Tag(path = "/DataTypes/Character", attribute = "value") + Character Character; + } + + public static class BooleanData { + public BooleanData(boolean boolTrue, boolean primitiveBoolTrue, boolean boolFalse, boolean primitiveBoolFalse) { + this.BooleanTrue = boolTrue; + this.booleanTrue = primitiveBoolTrue; + this.BooleanFalse = boolFalse; + this.booleanFalse = primitiveBoolFalse; + } + + @Tag(path = "/DataTypes/BooleanTrue", attribute = "Boolean") + boolean BooleanTrue; + + @Tag(path = "/DataTypes/BooleanTrue", attribute = "boolean") + boolean booleanTrue; + + @Tag(path = "/DataTypes/BooleanFalse", attribute = "Boolean") + boolean BooleanFalse; + + @Tag(path = "/DataTypes/BooleanFalse", attribute = "boolean") + boolean booleanFalse; + } + + public static class ParentHolder { + @Tag(path = "/DataTypes") + NestedField nestedField; + } + + public static class NestedField { + public static class StringData extends NestedField { + public StringData(String value) { + this.String = value; + } + + @Tag(path = "String", attribute = "value") + String String; + } + } +}