Skip to content

Commit

Permalink
feat: add preliminary serialization functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-grgt committed Jan 9, 2024
1 parent dc570fc commit 50ff43c
Show file tree
Hide file tree
Showing 22 changed files with 933 additions and 53 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ build/

### Mac OS ###
.DS_Store

### Jreleaser
out/
trace.log
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -12,20 +12,20 @@ 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

```xml
<dependency>
<groupId>io.jonasg</groupId>
<artifactId>xjx-serdes</artifactId>
<version>0.1.0</version>
<version>0.2.0</version>
</dependency>
```

Expand Down Expand Up @@ -79,7 +79,10 @@ String document = """
</WeatherData>""";


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.
Expand Down Expand Up @@ -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 <Country> within the specified path,
and the city field is serialized to <City><Name>.

## 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.
80 changes: 53 additions & 27 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>io.jonasg</groupId>
<artifactId>xjx</artifactId>
<version>0.1.1-SNAPSHOT</version>
<version>0.2.0</version>
<packaging>pom</packaging>
<modules>
<module>xjx-sax</module>
Expand Down Expand Up @@ -142,36 +142,62 @@
</pomElements>
</configuration>
</plugin>
<plugin>
<groupId>org.jreleaser</groupId>
<artifactId>jreleaser-maven-plugin</artifactId>
<configuration>
<jreleaser>
<signing>
<active>ALWAYS</active>
<armored>true</armored>
</signing>
<deploy>
<maven>
<nexus2>
<maven-central>
<active>ALWAYS</active>
<url>https://s01.oss.sonatype.org/service/local</url>
<snapshotUrl>https://s01.oss.sonatype.org/content/repositories/snapshots/</snapshotUrl>
<closeRepository>true</closeRepository>
<releaseRepository>true</releaseRepository>
<stagingRepositories>target/staging-deploy</stagingRepositories>
</maven-central>
</nexus2>
</maven>
</deploy>
</jreleaser>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.jreleaser</groupId>
<artifactId>jreleaser-maven-plugin</artifactId>
<inherited>false</inherited>
<configuration>
<jreleaser>
<project>
<description>Xjx - Lightweight XML Library for Java</description>
<links>
<homepage>https://github.com/jonasgeiregat/xjx</homepage>
</links>
<license>APACHE-2.0</license>
<authors>Jonas Geiregat</authors>
<copyright>2023 Jonas Geiregat</copyright>
</project>
<release>
<github>
<changelog>
<formatted>ALWAYS</formatted>
<preset>conventional-commits</preset>
</changelog>
</github>
</release>
<signing>
<active>ALWAYS</active>
<armored>true</armored>
</signing>
<deploy>
<maven>
<nexus2>
<maven-central>
<active>ALWAYS</active>
<url>https://s01.oss.sonatype.org/service/local</url>
<snapshotUrl>https://s01.oss.sonatype.org/content/repositories/snapshots/</snapshotUrl>
<closeRepository>false</closeRepository>
<releaseRepository>false</releaseRepository>
<stagingRepositories>target/staging-deploy</stagingRepositories>
</maven-central>
</nexus2>
</maven>
</deploy>
</jreleaser>
</configuration>
</plugin>

</plugins>
</build>
</profile>
<profile>
<id>publication</id>
<properties>
Expand Down
2 changes: 1 addition & 1 deletion xjx-sax/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>io.jonasg</groupId>
<artifactId>xjx</artifactId>
<version>0.1.1-SNAPSHOT</version>
<version>0.2.0</version>
</parent>

<artifactId>xjx-sax</artifactId>
Expand Down
7 changes: 6 additions & 1 deletion xjx-sax/src/main/java/io/jonasg/xjx/Attributes.java
Original file line number Diff line number Diff line change
@@ -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<String, String> attributes = new HashMap<>();
private final Map<String, String> attributes = new LinkedHashMap<>();

public Attributes(String... values) {
int length = values.length;
Expand Down Expand Up @@ -40,6 +41,10 @@ public Stream<Attribute> stream() {
.map(e -> new Attribute(e.getKey(), e.getValue()));
}

public boolean isEmpty() {
return attributes.isEmpty();
}


public record Attribute(String name, String value) {}

Expand Down
2 changes: 1 addition & 1 deletion xjx-serdes/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>io.jonasg</groupId>
<artifactId>xjx</artifactId>
<version>0.1.1-SNAPSHOT</version>
<version>0.2.0</version>
</parent>

<artifactId>xjx-serdes</artifactId>
Expand Down
35 changes: 35 additions & 0 deletions xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Section.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
21 changes: 19 additions & 2 deletions xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

/**
Expand Down Expand Up @@ -92,4 +96,17 @@ public <K, V> Map<K, V> read(Reader data, MapOf<K, V> 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 <T> The generic type of the object.
* @return The XML representation of the object.
*/
public <T> String write(T data) {
var nodes = xmlNodeStructureFactory.build(data);
return xmlStringBuilder.build(nodes);
}

}
Original file line number Diff line number Diff line change
@@ -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 <T> XmlNode build(T data) {
return getXmlNode(Path.parse("/"), data, null);
}

private <T> 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());
}
}

}
Loading

0 comments on commit 50ff43c

Please sign in to comment.