Skip to content

Commit b5f2ee8

Browse files
committed
feat: JsonResourcesModule
1 parent f39734c commit b5f2ee8

File tree

13 files changed

+739
-2
lines changed

13 files changed

+739
-2
lines changed

support/json/json-resources/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies {
1414
compile 'org.slf4j:slf4j-api:1.7.25'
1515

1616
testCompile project(':support:testing')
17+
testCompile 'org.slf4j:slf4j-simple:1.7.25'
1718
}
1819

1920
testlogger {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package sparkles.support.json.resources;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Annotation specifying a CURIE for use with links. As defined by W3C in
10+
* <a href="https://www.w3.org/TR/2010/NOTE-curie-20101216/">CURIE Syntax 1.0</a>. Note that in
11+
* the context of HAL the only substitution done to the template URI of a curie is the
12+
* <code>{rel}</code> place holder.
13+
*/
14+
@Target({ElementType.TYPE })
15+
@Retention(RetentionPolicy.RUNTIME)
16+
public @interface Curie {
17+
18+
/**
19+
* CURIE href template e.g. "http://docs.my.site/{rel}"
20+
* @return href a reference to the elaborated documentation for a given resource
21+
*/
22+
String href() default "";
23+
24+
/**
25+
* CURIE name used to reference the CURIE in {@link Link} annotations
26+
* e.g. "mysite"
27+
* @return the name of the curie
28+
*/
29+
String prefix() default "";
30+
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package sparkles.support.json.resources;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Annotation specifying an array of curies to be used in defining link relations.
10+
*/
11+
@Target({ElementType.TYPE })
12+
@Retention(RetentionPolicy.RUNTIME)
13+
public @interface Curies {
14+
15+
/**
16+
* Annotation grouping a list of {@link Curies} for convenience/readability
17+
* @return an array of curies
18+
*/
19+
Curie[] value() default {};
20+
21+
}

support/json/json-resources/src/main/java/sparkles/support/json/resources/JacksonResourcesModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
* This is a modified version of openapi-tools/jackson-dataformat-hal
7474
*
7575
* @link https://github.com/openapi-tools/jackson-dataformat-hal
76+
*
77+
* @deprecated Use {@link JsonResourcesModule} instead
7678
*/
7779
public class JacksonResourcesModule extends SimpleModule {
7880

support/json/json-resources/src/main/java/sparkles/support/json/resources/JsonResourcesModule.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,25 @@
33
import com.fasterxml.jackson.core.Version;
44
import com.fasterxml.jackson.databind.module.SimpleModule;
55

6+
import sparkles.support.json.resources.internal.DeserializerModifier;
7+
import sparkles.support.json.resources.internal.SerializerModifier;
8+
69
public class JsonResourcesModule extends SimpleModule {
710

811
public JsonResourcesModule() {
912
super(JsonResourcesModule.class.getSimpleName(), Version.unknownVersion());
1013
}
1114

15+
@Override
16+
public void setupModule(SetupContext context) {
17+
context.addBeanSerializerModifier(new SerializerModifier());
18+
context.addBeanDeserializerModifier(new DeserializerModifier());
19+
}
20+
1221
// TODO:
1322
// - inspire from https://github.com/openapi-tools/jackson-dataformat-hal
14-
// - add serializer + modifier
15-
// - add deserializer + modifier
23+
// (/) add serializer + modifier
24+
// (/) add deserializer + modifier
1625
// - change serialization and deserialization...
1726
// - recognize custom @Embedded annotation
1827
// - for @Links annotation and Link class
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package sparkles.support.json.resources.internal;
2+
3+
import java.net.URI;
4+
import java.util.Arrays;
5+
import java.util.Objects;
6+
import java.util.Optional;
7+
import java.util.StringTokenizer;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
10+
import sparkles.support.json.resources.Curie;
11+
12+
/**
13+
* Defining map of a curies in the context of HAL. That is, this will only substitute
14+
* a curie reference into the <code>{rel}</code> placeholder of a templated URI.
15+
*
16+
* @see <a href="https://www.w3.org/TR/2010/NOTE-curie-20101216/">CURIE Syntax 1.0</a>
17+
*/
18+
public class CurieMap {
19+
20+
private final ConcurrentHashMap<String, String> mappings = new ConcurrentHashMap<>();
21+
22+
23+
public CurieMap(Mapping... mappings) {
24+
Objects.requireNonNull(mappings, "Non-null array must be provided");
25+
Arrays.stream(mappings).forEach(m -> this.mappings.put(m.prefix, m.template));
26+
}
27+
28+
/**
29+
* Resolve the given curie using this mapping. Return empty if no relevant mapping could be found.
30+
*
31+
* @param curie Curie to resolve using this map in the standard form <code>prefix:rel</code>
32+
* @return Resolved URI if map contained mapping for the given prefix - empty otherwise
33+
*/
34+
public Optional<URI> resolve(String curie) {
35+
StringTokenizer st = new StringTokenizer(curie, ":");
36+
if (st.countTokens() != 2) {
37+
return Optional.empty();
38+
}
39+
40+
String template = mappings.get(st.nextToken());
41+
if (template == null) {
42+
return Optional.empty();
43+
} else {
44+
URI resolvedURI = URI.create(template.replace("{rel}", st.nextToken()));
45+
return Optional.of(resolvedURI);
46+
}
47+
}
48+
49+
/**
50+
* A single mapping definition in the map.
51+
*/
52+
public static class Mapping {
53+
private final String prefix;
54+
private final String template;
55+
56+
public Mapping(String prefix, String template) {
57+
this.prefix = prefix;
58+
this.template = template;
59+
}
60+
61+
public Mapping(Curie curie) {
62+
this.prefix = curie.prefix();
63+
this.template = curie.href();
64+
}
65+
}
66+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package sparkles.support.json.resources.internal;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.core.TreeNode;
5+
import com.fasterxml.jackson.databind.DeserializationContext;
6+
import com.fasterxml.jackson.databind.JsonDeserializer;
7+
import com.fasterxml.jackson.databind.JsonNode;
8+
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
9+
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
10+
import com.fasterxml.jackson.databind.node.ArrayNode;
11+
import com.fasterxml.jackson.databind.node.ObjectNode;
12+
13+
import java.io.IOException;
14+
import java.net.URI;
15+
import java.util.Iterator;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.stream.Collectors;
19+
import java.util.stream.StreamSupport;
20+
21+
/**
22+
* Deserializer to handle incoming application/hal+json. The de-serializer is responsible for intercepting
23+
* the reserved properties (<code>_links</code> and <code>_embedded</code>) and mapping the properties of these
24+
* objects in the incoming json to the uniquely assigned properties of the POJO class.
25+
*/
26+
public class Deserializer extends DelegatingDeserializer {
27+
28+
public Deserializer(BeanDeserializerBase delegate) {
29+
super(delegate);
30+
}
31+
32+
@Override
33+
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
34+
TreeNode tn = p.getCodec().readTree(p);
35+
if (tn.isObject()) {
36+
ObjectNode root = (ObjectNode) tn;
37+
for (ReservedProperty rp : ReservedProperty.values()) {
38+
ObjectNode on = (ObjectNode) tn.get(rp.getPropertyName());
39+
if (on != null) {
40+
CurieMap curieMap = createCurieMap(rp, on);
41+
on.remove("curies");
42+
43+
Iterator<Map.Entry<String, JsonNode>> it = on.fields();
44+
while (it.hasNext()) {
45+
Map.Entry<String, JsonNode> jn = it.next();
46+
String propertyName = curieMap.resolve(jn.getKey()).map(URI::toString).orElse(jn.getKey());
47+
root.set(rp.alternateName(propertyName), jn.getValue());
48+
}
49+
50+
root.remove(rp.getPropertyName());
51+
}
52+
53+
}
54+
}
55+
56+
final JsonParser modifiedParser = tn.traverse(p.getCodec());
57+
modifiedParser.nextToken();
58+
return _delegatee.deserialize(modifiedParser, ctxt);
59+
}
60+
61+
private CurieMap createCurieMap(ReservedProperty rp, ObjectNode on) {
62+
if (ReservedProperty.LINKS.equals(rp) && on.has("curies")) {
63+
ArrayNode curies = (ArrayNode) on.get("curies");
64+
List<CurieMap.Mapping> mappings = StreamSupport.stream(curies.spliterator(), false)
65+
.map(n -> createMapping((ObjectNode) n))
66+
.collect(Collectors.toList());
67+
return new CurieMap(mappings.toArray(new CurieMap.Mapping[0]));
68+
} else {
69+
return new CurieMap();
70+
}
71+
}
72+
73+
private CurieMap.Mapping createMapping(ObjectNode node) {
74+
return new CurieMap.Mapping(node.get("name").textValue(), node.get("href").textValue());
75+
}
76+
77+
@Override
78+
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
79+
return new Deserializer((BeanDeserializerBase) newDelegatee);
80+
}
81+
82+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package sparkles.support.json.resources.internal;
2+
3+
import com.fasterxml.jackson.databind.BeanDescription;
4+
import com.fasterxml.jackson.databind.DeserializationConfig;
5+
import com.fasterxml.jackson.databind.JsonDeserializer;
6+
import com.fasterxml.jackson.databind.PropertyName;
7+
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
8+
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
9+
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
10+
11+
import java.util.ArrayList;
12+
import java.util.Arrays;
13+
import java.util.Iterator;
14+
import java.util.List;
15+
16+
import sparkles.support.json.resources.Curie;
17+
import sparkles.support.json.resources.Curies;
18+
import sparkles.support.json.resources.Resource;
19+
20+
/**
21+
* Modify the deserialization of classes annotated with {@link Resource}. Deserialization will handle the reserved
22+
* properties <code>_links</code> and <code>_embedded</code> by assigning a unique property name to each of the
23+
* properties that are part of these sections.
24+
*/
25+
public class DeserializerModifier extends BeanDeserializerModifier {
26+
27+
@Override
28+
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
29+
Resource ann = beanDesc.getClassAnnotations().get(Resource.class);
30+
if (ann != null) {
31+
return new Deserializer((BeanDeserializer) deserializer);
32+
}
33+
return deserializer;
34+
}
35+
36+
@Override
37+
public List<BeanPropertyDefinition> updateProperties(DeserializationConfig config, BeanDescription beanDesc, List<BeanPropertyDefinition> propDefs) {
38+
Resource ann = beanDesc.getClassAnnotations().get(Resource.class);
39+
if (ann != null) {
40+
CurieMap map = createCurieMap(beanDesc);
41+
List<BeanPropertyDefinition> modified = new ArrayList<>();
42+
Iterator<BeanPropertyDefinition> properties = propDefs.iterator();
43+
while (properties.hasNext()) {
44+
BeanPropertyDefinition property = properties.next();
45+
for (ReservedProperty rp : ReservedProperty.values()) {
46+
String alternateName = rp.alternateName(property, map);
47+
if (!property.getName().equals(alternateName)) {
48+
modified.add(property.withName(new PropertyName(alternateName)));
49+
properties.remove();
50+
}
51+
}
52+
}
53+
propDefs.addAll(modified);
54+
}
55+
return propDefs;
56+
}
57+
58+
private CurieMap createCurieMap(BeanDescription beanDesc) {
59+
ArrayList<CurieMap.Mapping> mappings = new ArrayList<>();
60+
Curie sc = beanDesc.getClassAnnotations().get(Curie.class);
61+
if (sc != null) {
62+
mappings.add(new CurieMap.Mapping(sc));
63+
}
64+
Curies cs = beanDesc.getClassAnnotations().get(Curies.class);
65+
if (cs != null) {
66+
Arrays.stream(cs.value()).forEach(c -> mappings.add(new CurieMap.Mapping(c)));
67+
}
68+
return new CurieMap(mappings.toArray(new CurieMap.Mapping[0]));
69+
}
70+
71+
}

0 commit comments

Comments
 (0)