Skip to content

Commit aeabb09

Browse files
authored
Merge pull request #7 from openapi-tools/add-curies-support
Add curies support as a continuation of Jim Hooker PR #6. Solving issue #4.
2 parents eddef1e + 160005e commit aeabb09

File tree

16 files changed

+904
-58
lines changed

16 files changed

+904
-58
lines changed

README.md

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ the normal Jackson JSON handling.
1111
Module is considered production ready.
1212

1313
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.openapitools.jackson.dataformat/jackson-dataformat-hal/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.openapitools.jackson.dataformat/jackson-dataformat-hal/)
14-
[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/io.openapitools.jackson.dataformat/jackson-dataformat-hal/badge.svg)](https://www.javadoc.io/doc/io.openapitools.jackson.dataformat/jackson-dataformat-hal)
14+
[![Javadoc](http://javadoc.io/badge/io.openapitools.jackson.dataformat/jackson-dataformat-hal.svg)](https://www.javadoc.io/doc/io.openapitools.jackson.dataformat/jackson-dataformat-hal)
1515
[![Build status](https://travis-ci.org/openapi-tools/jackson-dataformat-hal.svg?branch=master)](https://travis-ci.org/openapi-tools/jackson-dataformat-hal)
1616
[![Coverage Status](https://codecov.io/gh/openapi-tools/jackson-dataformat-hal/coverage.svg?branch=master)](https://codecov.io/gh/openapi-tools/jackson-dataformat-hal)
1717

@@ -71,6 +71,114 @@ The JSON resulting from the above small POJO model would look like the following
7171

7272
All fields which are not annotated will be handled by the normal Jackson JSON data-binding.
7373

74+
## Including Curies
75+
76+
```java
77+
@Resource
78+
@Curie(curie = "curie", href = "http://example.com/doc/{rel}")
79+
class Model {
80+
String modelProperty;
81+
82+
@Link
83+
HALLink self;
84+
85+
@Link("rel:associated")
86+
HALLink relation;
87+
88+
@Link(value = "curieLink", curie = "curie")
89+
HALLink curieLink;
90+
91+
}
92+
```
93+
94+
```java
95+
@Resource
96+
@Curies({
97+
@Curie(href = "http://docs.my.site/{rel}", curie = "curie1"),
98+
@Curie(href = "http://docs.other.site/{rel}", curie = "curie2")})
99+
100+
class Model {
101+
String modelProperty;
102+
103+
@Link
104+
HALLink self;
105+
106+
@Link("rel:associated")
107+
HALLink relation;
108+
109+
@Link(value = "curieLink11", curie = "curie1")
110+
HALLink curieLink11;
111+
112+
@Link(value = "curieLink21", curie = "curie2")
113+
HALLink curieLink21;
114+
115+
@Link(value = "curieLink22", curie = "curie2")
116+
HALLink curieLink22;
117+
118+
@Link(value = "curieLink23", curie = "curie2")
119+
HALLink curieLink23;
120+
121+
}
122+
```
123+
The resulting JSON would be:
124+
125+
```json
126+
{
127+
"_links": {
128+
"curies": [{
129+
"name": "curie1",
130+
"href": "http://docs.my.site/{rel}",
131+
"templated": "true"
132+
},{
133+
"name": "curie2",
134+
"href": "http://docs.other.site/{rel}",
135+
"templated": "true"
136+
}],
137+
"self": { "href": "https://..."},
138+
"rel:associated": { "href": "https://..."},
139+
"curie1:link11": { "href": "https://...", "templated" : "..."},
140+
"curie2:link21": { "href": "https://...", "templated" : "..."},
141+
"curie2:link22": { "href": "https://...", "templated" : "..."}
142+
},
143+
"_embedded": {
144+
"associated": {
145+
"_links": {
146+
"self": { "href": "https://..." }
147+
},
148+
"associatedProperty": "..."
149+
}
150+
},
151+
"modelProperty": "..."
152+
}
153+
```
154+
155+
The above is equivalent to:
156+
157+
```json
158+
{
159+
"_links": {
160+
"self": { "href": "https://..."},
161+
"rel:associated": { "href": "https://..."},
162+
"http://docs.my.site/link11": { "href": "https://...", "templated" : "..."},
163+
"http://docs.other.site/link21": { "href": "https://...", "templated" : "..."},
164+
"http://docs.other.site/link22": { "href": "https://...", "templated" : "..."}
165+
},
166+
"_embedded": {
167+
"associated": {
168+
"_links": {
169+
"self": { "href": "https://..." }
170+
},
171+
"associatedProperty": "..."
172+
}
173+
},
174+
"modelProperty": "..."
175+
}
176+
```
177+
178+
Both will be supported for deserialization. Also if curie prefixes in the incoming document
179+
is chosen to be different from the prefixes in the POJO annotations deserialization will be
180+
supported.
181+
74182
## Serializing POJOs as HAL JSON
75183

76184
Serialization is similar to the normal JSON serialization using the `HALMapper` instead of the

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@
9494
<artifactId>maven-compiler-plugin</artifactId>
9595
<version>3.5.1</version>
9696
<configuration>
97-
<source>1.7</source>
98-
<target>1.7</target>
97+
<source>1.8</source>
98+
<target>1.8</target>
9999
<compilerArgs>
100100
<arg>-Xlint</arg>
101101
</compilerArgs>

src/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ public static class Builder {
152152
*/
153153
public Builder(String href) {
154154
this.href = href;
155-
templated = true;
155+
templated = getTemplated(href);
156156
}
157157

158158
/**
@@ -212,5 +212,14 @@ public Builder seen(Instant temporal) {
212212
public HALLink build() {
213213
return new HALLink(this);
214214
}
215+
216+
/**
217+
* URI template https://tools.ietf.org/html/rfc6570
218+
*
219+
* very simple implementation of templates
220+
*/
221+
private Boolean getTemplated(String href) {
222+
return href.contains("{");
223+
}
215224
}
216225
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.openapitools.jackson.dataformat.hal.annotation;
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 io.openapitools.jackson.dataformat.hal.annotation;
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+
}

src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@
1616
* @return name of the relation represented by the link.
1717
*/
1818
String value() default "";
19+
20+
/**
21+
* CURIE prefix - if not set then no CURIE will be associated.
22+
* @return name of the CURIE intended to be used with this link.
23+
*/
24+
String curie() default "";
1925
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.openapitools.jackson.dataformat.hal.deser;
2+
3+
import io.openapitools.jackson.dataformat.hal.annotation.Curie;
4+
import java.net.URI;
5+
import java.util.Arrays;
6+
import java.util.Objects;
7+
import java.util.Optional;
8+
import java.util.StringTokenizer;
9+
import java.util.concurrent.ConcurrentHashMap;
10+
11+
/**
12+
* Defining map of a curies in the context of HAL. That is, this will only substitute
13+
* a curie reference into the <code>{rel}</code> placeholder of a templated URI.
14+
*
15+
* @see <a href="https://www.w3.org/TR/2010/NOTE-curie-20101216/">CURIE Syntax 1.0</a>
16+
*/
17+
public class CurieMap {
18+
19+
private final ConcurrentHashMap<String, String> mappings = new ConcurrentHashMap<>();
20+
21+
22+
public CurieMap(Mapping... mappings) {
23+
Objects.requireNonNull(mappings, "Non-null array must be provided");
24+
Arrays.stream(mappings).forEach(m -> this.mappings.put(m.prefix, m.template));
25+
}
26+
27+
/**
28+
* Resolve the given curie using this mapping. Return empty if no relevant mapping could be found.
29+
*
30+
* @param curie Curie to resolve using this map in the standard form <code>prefix:rel</code>
31+
* @return Resolved URI if map contained mapping for the given prefix - empty otherwise
32+
*/
33+
public Optional<URI> resolve(String curie) {
34+
StringTokenizer st = new StringTokenizer(curie, ":");
35+
if (st.countTokens() != 2) {
36+
return Optional.empty();
37+
}
38+
39+
String template = mappings.get(st.nextToken());
40+
if (template == null) {
41+
return Optional.empty();
42+
} else {
43+
URI resolvedURI = URI.create(template.replace("{rel}", st.nextToken()));
44+
return Optional.of(resolvedURI);
45+
}
46+
}
47+
48+
/**
49+
* A single mapping definition in the map.
50+
*/
51+
public static class Mapping {
52+
private final String prefix;
53+
private final String template;
54+
55+
public Mapping(String prefix, String template) {
56+
this.prefix = prefix;
57+
this.template = template;
58+
}
59+
60+
public Mapping(Curie curie) {
61+
this.prefix = curie.prefix();
62+
this.template = curie.href();
63+
}
64+
}
65+
}

src/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializer.java

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@
88
import com.fasterxml.jackson.databind.JsonNode;
99
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
1010
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
11+
import com.fasterxml.jackson.databind.node.ArrayNode;
1112
import com.fasterxml.jackson.databind.node.ObjectNode;
1213
import java.io.IOException;
14+
import java.net.URI;
1315
import java.util.Iterator;
16+
import java.util.List;
1417
import java.util.Map;
18+
import java.util.stream.Collectors;
19+
import java.util.stream.StreamSupport;
1520

1621
/**
17-
* Deserializer to handle incomming application/hal+json.
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.
1825
*/
1926
public class HALBeanDeserializer extends DelegatingDeserializer {
2027

@@ -23,21 +30,26 @@ public HALBeanDeserializer(BeanDeserializerBase delegate) {
2330
}
2431

2532
@Override
26-
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
27-
TreeNode tn = p.getCodec().readTree(p);
33+
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
34+
TreeNode tn = p.getCodec().readTree(p);
2835
if (tn.isObject()) {
2936
ObjectNode root = (ObjectNode) tn;
3037
for (ReservedProperty rp : ReservedProperty.values()) {
3138
ObjectNode on = (ObjectNode) tn.get(rp.getPropertyName());
3239
if (on != null) {
33-
Iterator<Map.Entry<String,JsonNode>> it = on.fields();
40+
CurieMap curieMap = createCurieMap(rp, on);
41+
on.remove("curies");
42+
43+
Iterator<Map.Entry<String, JsonNode>> it = on.fields();
3444
while (it.hasNext()) {
35-
Map.Entry<String,JsonNode> jn = it.next();
36-
root.set(rp.alternateName(jn.getKey()), jn.getValue());
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());
3748
}
38-
root.remove(rp.getPropertyName());
49+
50+
root.remove(rp.getPropertyName());
3951
}
40-
52+
4153
}
4254
}
4355

@@ -46,9 +58,25 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
4658
return _delegatee.deserialize(modifiedParser, ctxt);
4759
}
4860

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+
4977
@Override
5078
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
5179
return new HALBeanDeserializer((BeanDeserializerBase) newDelegatee);
5280
}
53-
81+
5482
}

0 commit comments

Comments
 (0)