Skip to content

Commit

Permalink
feat: support simple generic arguments for List and Set mappings
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-grgt committed Jan 15, 2024
1 parent 01ca574 commit 2f21ca6
Show file tree
Hide file tree
Showing 15 changed files with 549 additions and 283 deletions.
87 changes: 36 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
```
Expand All @@ -68,7 +68,7 @@ String document = """
<CurrentConditions>
<Temperature>
<Value>75</Value>
<Unit>°F</Unit>
<Unit><![CDATA[°F]]></Unit>
</Temperature>
<Humidity>
<Value>60</Value>
Expand Down Expand Up @@ -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
<?xml version="1.0" encoding="UTF-8"?>
<WeatherData>
<Forecasts>
<Day Date="2023-09-12">
<High>
<Value>71</Value>
</High>
<Low>
<Value>62</Value>
</Low>
</Day>
<Forecasts>
<Day Date="2023-09-12">
<High>
<Value>71</Value>
</High>
<Low>
<Value>62</Value>
</Low>
</Day>
<Day Date="2023-09-13">
<High>
<Value>78</Value>
</High>
<Low>
<Value>71</Value>
</Low>
<High>
<Value>78</Value>
</High>
<Low>
<Value>71</Value>
</Low>
</Day>
</Forecasts>
</WeatherData>
```

```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<Forecast> 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
<?xml version="1.0" encoding="UTF-8"?>
<WeatherData>
<CurrentConditions>
<Temperature>
<Value>75</Value>
<Unit>°F</Unit>
</Temperature>
</CurrentConditions>
<CurrentConditions>
<Temperature>
<Value>75</Value>
<Unit>°F</Unit>
</Temperature>
</CurrentConditions>
</WeatherData>
```

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
33 changes: 1 addition & 32 deletions xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Section.java
Original file line number Diff line number Diff line change
@@ -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) {
}
62 changes: 62 additions & 0 deletions xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Tag.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>
* Example XML document:
* <pre> {@code
* <Products>
* <Name>Product 1</Name>
* <Name>Product 2</Name>
* <Name>Product 3</Name>
* </Products>
* }</pre>
* </p>
* <p>
* Example Usage:
* <pre>{@code
* @Tag(path = "/Products", items = "Name")
* List<String> productNames;
* }</pre>
* In this example, the {@code List<String>} field 'productNames' will be serialized to and deserialized from the XML path "/Products/Name".
* </p>
*
* <p>
* Example XML for Serialization:
* <pre>{@code
* <Products>
* <Name>Product 1</Name>
* <Name>Product 2</Name>
* <Name>Product 3</Name>
* </Products>
* }</pre>
* In this example, when the {@code List<String>} field 'productNames' is serialized, the generated XML will look like the above representation.
* </p>
*
* <p>
* Annotation Usage:
* <ul>
* <li>{@code path}: Specifies the Path expression indicating the location of the XML data for serialization and deserialization.</li>
* <li>{@code attribute}: Specifies the name of an XML attribute to be used during serialization and deserialization (optional).</li>
* <li>{@code items}: Specifies additional information for serializing and deserializing items within a collection (optional).</li>
* </ul>
* </p>
*
*/
@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 "";
}
89 changes: 89 additions & 0 deletions xjx-serdes/src/main/java/io/jonasg/xjx/serdes/TypeMappers.java
Original file line number Diff line number Diff line change
@@ -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<Class<Double>> DOUBLE_TYPES = List.of(double.class, Double.class);
static List<Class<Long>> LONG_TYPES = List.of(long.class, Long.class);
static List<Class<Character>> CHAR_TYPES = List.of(char.class, Character.class);
static List<Class<Boolean>> BOOLEAN_TYPES = List.of(boolean.class, Boolean.class);

public static Set<Class<?>> 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<Object, Object> forType(Class<?> type) {
Function<Object, Object> 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 extends Enum<T>> 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;
}
}
2 changes: 2 additions & 0 deletions xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 2f21ca6

Please sign in to comment.