Skip to content

Commit

Permalink
feat(xjx-serdes): add better map support
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-grgt committed Dec 25, 2023
1 parent 7d45033 commit 5be835c
Show file tree
Hide file tree
Showing 17 changed files with 559 additions and 118 deletions.
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class Temperature {
### Attributes


### Collections
### Collection types
When deserializing XML data containing a collection type, the following conventions apply:

- Only `List` and `Set` types are supported
Expand Down Expand Up @@ -179,3 +179,59 @@ public class Forecast {
String minTemperature;
}
```

### Map types

Maps can be deserialized either as a field or a top-level type. Consider the following XML document:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<WeatherData>
<CurrentConditions>
<Temperature>
<Value>75</Value>
<Unit>°F</Unit>
</Temperature>
</CurrentConditions>
</WeatherData>
```

#### Option 1: Map a Specific Section

You can map a specific section from the XML onto a custom field:

```java
class WeatherData {
@Tag(path = "/WeatherData/CurrentConditions")
Map<String, Object> map;
}
```
In this case, the map field will contain:
```java
Map.of("Temperature", Map.of("Value", "75", "Unit", "°F"));
```

#### Option 2: Map the Whole Document

Alternatively, you can map the entire document onto a Map of String Object
```java
class WeatherData {
@Tag(path = "/WeatherData")
Map<String, Object> map;
}
```
In this case, the map field will contain:
```java
Map.of("CurrentConditions",
Map.of("Temperature", Map.of("Value", "75", "Unit", "°F"))));
```

#### Option 3: Map to a Map
```java
Map<String, Object> map = new XjxSerdes().read(document, new MapOf<>() {});
```
In this case, the result of `read` will contain a Map of String Object
```java
Map.of("CurrentConditions",
Map.of("Temperature", Map.of("Value", "75", "Unit", "°F"))));
```

14 changes: 14 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
<module>xjx-serdes</module>
</modules>

<name>xjx</name>
<description>Lightweight XML library for Java</description>
<url>https://github.com/jonasgeiregat/xjx</url>

<licenses>
<license>
<name>GPL-v3.0</name>
Expand Down Expand Up @@ -127,6 +131,16 @@
</goals>
</execution>
</executions>
<configuration>
<pomElements>
<url/>
<name/>
<description/>
<scm/>
<developers/>
<inceptionYear/>
</pomElements>
</configuration>
</plugin>
<plugin>
<groupId>org.jreleaser</groupId>
Expand Down
3 changes: 3 additions & 0 deletions xjx-sax/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
</parent>

<artifactId>xjx-sax</artifactId>
<name>xjx-sax</name>
<description>SAX based XML parser</description>
<url>https://github.com/jonasgeiregat/xjx</url>

<properties>
<maven.compiler.source>17</maven.compiler.source>
Expand Down
3 changes: 0 additions & 3 deletions xjx-sax/src/main/java/io/jonasg/xjx/EndTag.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,4 @@

public record EndTag(String namespace, String name) {

public EndTag(String name) {
this(null, name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.io.StringReader;
import java.util.ArrayList;

import static org.assertj.core.api.Assertions.*;

class EndTagScannerTest {


Expand All @@ -22,7 +24,7 @@ void shouldFail_whenInputDoesNotStartWithALessThenSignFollowedByASlash() {
ThrowableAssert.ThrowingCallable act = () -> endTagAction.scan(new BufferedPositionedReader(new StringReader("home</a>")), t -> {});

// then
Assertions.assertThatThrownBy(act)
assertThatThrownBy(act)
.isInstanceOf(XmlParsingException.class)
.hasMessage("End tag does not start with </ in: 'home</a>'");
}
Expand All @@ -37,7 +39,7 @@ void shouldEmit_endTag() {
endTagAction.scan(new BufferedPositionedReader(new StringReader("</a>")), tokens::add);

// then
Assertions.assertThat(tokens).containsExactly(new Token<>(Token.Type.END_TAG, new EndTag("a")));
assertThat(tokens).containsExactly(new Token<>(Token.Type.END_TAG, new EndTag(null, "a")));
}


Expand All @@ -51,7 +53,7 @@ void shouldEmit_endTagWithNamespace() {
endTagAction.scan(new BufferedPositionedReader(new StringReader("</a:b>")), tokens::add);

// then
Assertions.assertThat(tokens).containsExactly(new Token<>(Token.Type.END_TAG, new EndTag("a", "b")));
assertThat(tokens).containsExactly(new Token<>(Token.Type.END_TAG, new EndTag("a", "b")));
}

}
3 changes: 3 additions & 0 deletions xjx-serdes/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
</parent>

<artifactId>xjx-serdes</artifactId>
<name>xjx-serdes</name>
<description>XML Serializer and Deserializer library for Java</description>
<url>https://github.com/jonasgeiregat/xjx</url>

<properties>
<maven.compiler.source>17</maven.compiler.source>
Expand Down
22 changes: 22 additions & 0 deletions xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package io.jonasg.xjx.serdes;

import io.jonasg.xjx.sax.SaxParser;
import io.jonasg.xjx.serdes.deserialize.MapOf;
import io.jonasg.xjx.serdes.deserialize.MapRootSaxHandler;
import io.jonasg.xjx.serdes.deserialize.PathBasedSaxHandler;
import io.jonasg.xjx.serdes.deserialize.PathWriterIndexFactory;
import io.jonasg.xjx.serdes.deserialize.XjxDeserializationException;

import java.io.Reader;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;

public class XjxSerdes {

Expand All @@ -31,4 +36,21 @@ public <T> T read(Reader data, Class<T> clazz) {
saxParser.parse(data, saxHandler);
return saxHandler.instance();
}

public <K, V> Map<K, V> read(String data, MapOf<K, V> mapOf) {
return read(new StringReader(data), mapOf);
}

@SuppressWarnings("unchecked")
public <K, V> Map<K, V> read(Reader data, MapOf<K, V> mapOf) {
Class<?> keyType = mapOf.keyType();
Class<?> valueType = mapOf.valueType();
if (keyType == String.class && valueType == Object.class) {
HashMap<String, Object> map = new HashMap<>();
MapRootSaxHandler mapRootSaxHandler = new MapRootSaxHandler(map, true);
saxParser.parse(data, mapRootSaxHandler);
return (Map<K, V>) map;
}
throw new XjxDeserializationException("Maps only support String as key");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.jonasg.xjx.serdes.deserialize;

import java.util.Map;

public record MapAsRoot(Object root, Map<String, Object> map) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.jonasg.xjx.serdes.deserialize;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public abstract class MapOf<K, V> {

protected final Type keyType;

protected final Type valueType;

protected MapOf() {
Type superClass = this.getClass().getGenericSuperclass();
this.keyType = ((ParameterizedType) superClass).getActualTypeArguments()[0];
this.valueType = ((ParameterizedType) superClass).getActualTypeArguments()[1];
}

@SuppressWarnings("unchecked")
public Class<K> keyType() {
return (Class<K>) keyType;
}

@SuppressWarnings("unchecked")
public Class<V> valueType() {
return (Class<V>) valueType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,31 @@
import java.util.List;
import java.util.Map;

public class MapSaxHandler implements SaxHandler {
public class MapRootSaxHandler implements SaxHandler {

private final LinkedList<Map<String, Object>> mapsStack;

private final boolean skipRootTag;

private Map<String, Object> instance;

private String characterData;

private String prevStartTag;

public MapSaxHandler(HashMap<String, Object> instance) {
private String rootTag;

public MapRootSaxHandler(Map<String, Object> instance) {
this.mapsStack = new LinkedList<>();
this.mapsStack.add(instance);
this.skipRootTag = false;
this.instance = instance;
}

public MapRootSaxHandler(HashMap<String, Object> instance, boolean skipRootTag) {
this.mapsStack = new LinkedList<>();
this.mapsStack.add(instance);
this.skipRootTag = skipRootTag;
}

@Override
Expand All @@ -28,11 +42,16 @@ public void startDocument() {

@Override
public void startTag(String namespace, String name, List<Attribute> attributes) {
Map<String, Object> activeMap = this.mapsStack.getLast();
Map<String, Object> newMap = new LinkedHashMap<>();
activeMap.put(name, newMap);
this.mapsStack.add(newMap);
this.prevStartTag = name;
if (this.rootTag != null || !skipRootTag) {
Map<String, Object> activeMap = this.mapsStack.getLast();
Map<String, Object> newMap = new LinkedHashMap<>();
activeMap.put(name, newMap);
this.mapsStack.add(newMap);
this.prevStartTag = name;
}
if (this.rootTag == null) {
this.rootTag = name;
}
}

@Override
Expand All @@ -53,4 +72,8 @@ public void endTag(String namespace, String name) {
public void characters(String data) {
this.characterData = data;
}

public Map<String, Object> instance() {
return instance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.jonasg.xjx.serdes.deserialize;

import java.util.HashMap;
import java.util.Map;

public record MapWithTypeInfo(Map<String, Object> map, Class<?> valueType) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,8 @@ public int hashCode() {
public String toString() {
return "/" + String.join("/", sections) + (attribute == null ? "" : "[" + attribute + "]");
}

public boolean isRoot() {
return sections.size() == 1;
}
}
Loading

0 comments on commit 5be835c

Please sign in to comment.