diff --git a/pom.xml b/pom.xml index b3a1269..c9f79f2 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,13 @@ test + + commons-io + commons-io + 2.5 + test + + diff --git a/src/main/java/com/github/jasminb/jsonapi/ResolutionStrategy.java b/src/main/java/com/github/jasminb/jsonapi/ResolutionStrategy.java new file mode 100644 index 0000000..9be56b4 --- /dev/null +++ b/src/main/java/com/github/jasminb/jsonapi/ResolutionStrategy.java @@ -0,0 +1,21 @@ +package com.github.jasminb.jsonapi; + +/** + * Represents different strategies for resolving relationships. + */ +public enum ResolutionStrategy { + + /** + * Strategy which resolves the relationship URL (as specified by the {@code relType} attribute on the + * {@code Relationship}) using a {@link RelationshipResolver}, and subsequently deserializes the JSON response into + * a Java object. + */ + OBJECT, + + /** + * Strategy which simply stores the relationship URL (as specified by the {@code relType} attribute on the + * {@code Relationship}) in String field on an object. + */ + REF + +} diff --git a/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java b/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java index 15fec91..b2aec52 100644 --- a/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java +++ b/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java @@ -373,6 +373,18 @@ private void handleRelationships(JsonNode source, Object object) String link; if (linkNode != null && ((link = getLink(linkNode)) != null)) { + if (configuration.getFieldRelationship(relationshipField).strategy() == ResolutionStrategy.REF) { + if (String.class.isAssignableFrom(relationshipField.getType())) { + relationshipField.set(object, link); + continue; + } + + throw new IllegalArgumentException("Reference resolution strategy requires String " + + "type, but " + relationshipField.getDeclaringClass().getName() + "#" + + relationshipField.getName() + " has type " + + relationshipField.getType().getName()); + } + if (isCollection(relationship)) { relationshipField.set(object, readDocumentCollection(resolver.resolve(link), type).get()); diff --git a/src/main/java/com/github/jasminb/jsonapi/annotations/Relationship.java b/src/main/java/com/github/jasminb/jsonapi/annotations/Relationship.java index c519713..ece674f 100644 --- a/src/main/java/com/github/jasminb/jsonapi/annotations/Relationship.java +++ b/src/main/java/com/github/jasminb/jsonapi/annotations/Relationship.java @@ -1,6 +1,7 @@ package com.github.jasminb.jsonapi.annotations; import com.github.jasminb.jsonapi.RelType; +import com.github.jasminb.jsonapi.ResolutionStrategy; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -20,4 +21,5 @@ boolean resolve() default false; boolean serialise() default true; RelType relType() default RelType.SELF; + ResolutionStrategy strategy() default ResolutionStrategy.OBJECT; } diff --git a/src/test/java/com/github/jasminb/jsonapi/ProbeResolver.java b/src/test/java/com/github/jasminb/jsonapi/ProbeResolver.java new file mode 100644 index 0000000..0fd2e33 --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/ProbeResolver.java @@ -0,0 +1,63 @@ +package com.github.jasminb.jsonapi; + +import java.util.HashMap; +import java.util.Map; + +/** + * Simple global RelationshipResolver implementation that maintains a count of responses for each resolution of a + * relationship url. + */ +public class ProbeResolver implements RelationshipResolver { + + /** + * Map of relationship urls to the response JSON + */ + private Map responseMap; + + /** + * Map of relationship to a count of the times they have been resolved + */ + private Map resolved = new HashMap<>(); + + /** + * Construct a new instance, supplying a Map of relationship URLs to a String of serialized JSON. + * + * @param responseMap response JSON keyed by relationship url + */ + public ProbeResolver(Map responseMap) { + this.responseMap = responseMap; + for (String url : responseMap.keySet()) { + resolved.put(url, 0); + } + } + + /** + * {@inheritDoc} + *

+ * If the supplied {@code relationshipURL} is missing from the response Map, then an + * {@code IllegalArgumentException} is thrown. + *

+ * + * @param relationshipURL URL. eg. users/1 or https://api.myhost.com/uers/1 + * @return + * @throws IllegalArgumentException if {@code relationshipURL} is missing from the response Map. + */ + @Override + public byte[] resolve(String relationshipURL) { + if (responseMap.containsKey(relationshipURL)) { + resolved.put(relationshipURL, resolved.get(relationshipURL)+1); + return responseMap.get(relationshipURL).getBytes(); + } + throw new IllegalArgumentException("Unable to resolve + relationshipURL + , missing response map " + + "entry."); + } + + /** + * Returns a map of relationship URLs and the number of times each URL was resolved. + * + * @return the resolution map + */ + public Map getResolved() { + return resolved; + } +} \ No newline at end of file diff --git a/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java b/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java index 0d37d83..a9f217f 100644 --- a/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java +++ b/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java @@ -324,7 +324,10 @@ public void testLinkObjectsAndRelType() throws Exception { Comment.class); underTest.setGlobalResolver(resolver); - List
articles = underTest.readObjectCollection(apiResponse.getBytes(), Article.class); + JSONAPIDocument> responseDocument = underTest + .readDocumentCollection(apiResponse.getBytes(), Article.class); + Assert.assertNotNull(responseDocument); + List
articles = responseDocument.get(); // Sanity check Assert.assertNotNull(articles); diff --git a/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Article.java b/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Article.java new file mode 100644 index 0000000..7d80be9 --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Article.java @@ -0,0 +1,56 @@ +package com.github.jasminb.jsonapi.resolutionstrategy; + + +import com.github.jasminb.jsonapi.RelType; +import com.github.jasminb.jsonapi.ResolutionStrategy; +import com.github.jasminb.jsonapi.annotations.Id; +import com.github.jasminb.jsonapi.annotations.Relationship; +import com.github.jasminb.jsonapi.annotations.Type; + +import java.util.List; + +@Type("articles") +public class Article { + @Id + private String id; + + private String title; + + @Relationship(value = "author", resolve = true, relType = RelType.RELATED, strategy = ResolutionStrategy.REF) + private String author; + + @Relationship(value = "comments", resolve = true, relType = RelType.RELATED, strategy = ResolutionStrategy.OBJECT) + private List comments; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public List getComments() { + return comments; + } + + public void setComments(List comments) { + this.comments = comments; + } +} diff --git a/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Author.java b/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Author.java new file mode 100644 index 0000000..5efefcc --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Author.java @@ -0,0 +1,46 @@ +package com.github.jasminb.jsonapi.resolutionstrategy; + + +import com.github.jasminb.jsonapi.annotations.Id; +import com.github.jasminb.jsonapi.annotations.Type; + +@Type("people") +public class Author { + @Id + private String id; + private String firstName; + private String lastName; + private String twitter; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getTwitter() { + return twitter; + } + + public void setTwitter(String twitter) { + this.twitter = twitter; + } +} diff --git a/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Comment.java b/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Comment.java new file mode 100644 index 0000000..f657900 --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Comment.java @@ -0,0 +1,42 @@ +package com.github.jasminb.jsonapi.resolutionstrategy; + + +import com.github.jasminb.jsonapi.RelType; +import com.github.jasminb.jsonapi.ResolutionStrategy; +import com.github.jasminb.jsonapi.annotations.Id; +import com.github.jasminb.jsonapi.annotations.Relationship; +import com.github.jasminb.jsonapi.annotations.Type; + +@Type("comments") +public class Comment { + @Id + private String id; + private String body; + + @Relationship(value = "author", resolve = true, relType = RelType.RELATED, strategy = ResolutionStrategy.REF) + private String author; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } +} diff --git a/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Foo.java b/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Foo.java new file mode 100644 index 0000000..2de7340 --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/Foo.java @@ -0,0 +1,46 @@ +package com.github.jasminb.jsonapi.resolutionstrategy; + +import com.github.jasminb.jsonapi.ResolutionStrategy; +import com.github.jasminb.jsonapi.annotations.Id; +import com.github.jasminb.jsonapi.annotations.Relationship; +import com.github.jasminb.jsonapi.annotations.Type; + +/** + * Contains a @Relationship field that is resolved to a reference, but it assigned to a non-String field. Relationships + * that are resolved to a reference require a {@code String} type to store the reference. + */ +@Type("foo") +public class Foo { + + @Id + private String id; + + private String name; + + @Relationship(value = "bar", resolve = true, strategy = ResolutionStrategy.REF) + private Integer bar; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getBar() { + return bar; + } + + public void setBar(Integer bar) { + this.bar = bar; + } +} diff --git a/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/ResolutionStrategyTest.java b/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/ResolutionStrategyTest.java new file mode 100644 index 0000000..0a2479a --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/resolutionstrategy/ResolutionStrategyTest.java @@ -0,0 +1,105 @@ +package com.github.jasminb.jsonapi.resolutionstrategy; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.github.jasminb.jsonapi.JSONAPIDocument; +import com.github.jasminb.jsonapi.ProbeResolver; +import com.github.jasminb.jsonapi.ResourceConverter; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Insures that the @Relationship attribute {@code relationshipStrategy} is properly used to resolve relationship links + * to objects ({@code ResolutionStrategy#OBJECT}) or to String references ({@code ResolutionStrategy#REF)}. + */ +public class ResolutionStrategyTest { + + private static final String COMMENTS_REL_LINK = "http://example.com/articles/1/comments"; + + private static final String COMMENT_1_ID = "5"; + + private static final String COMMENT_1_BODY = "First!"; + + private static final String COMMENT_1_AUTHOR_REL_LINK = "http://example.com/comments/5/author"; + + private static final String COMMENT_2_ID = "12"; + + private static final String COMMENT_2_BODY = "I like XML better"; + + private static final String COMMENT_2_AUTHOR_REL_LINK = "http://example.com/comments/12/author"; + + private static final String ARTICLES_AUTHOR_REL_LINK = "http://example.com/articles/1/author"; + + + /** + * Insures that {@link com.github.jasminb.jsonapi.ResolutionStrategy} is properly used to resolve objects or string + * references. In this test, the {@code Article} uses an object resolution strategy when resolving {@code Comment} + * objects, and records {@code String} references (i.e. uses a reference resolution strategy) when resolving + * {@code Author} objects. + * + * @throws Exception + */ + @Test + public void testResolutionStrategy() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); + + Map responseMap = new HashMap<>(); + responseMap.put(ARTICLES_AUTHOR_REL_LINK, ""); + responseMap.put(COMMENTS_REL_LINK, org.apache.commons.io.IOUtils.toString( + this.getClass().getResource("comments-response.json"), "UTF-8")); + ProbeResolver resolver = new ProbeResolver(responseMap); + + ResourceConverter underTest = new ResourceConverter(mapper, Article.class, Comment.class); + underTest.setGlobalResolver(resolver); + + JSONAPIDocument> responseDocument = underTest.readDocumentCollection( + org.apache.commons.io.IOUtils.toString( + this.getClass().getResource("articles.json"), "UTF-8") + .getBytes(), + Article.class); + Assert.assertNotNull(responseDocument); + List
articles = responseDocument.get(); + + // Sanity checks + Assert.assertNotNull(articles); + Assert.assertEquals(1, articles.size()); + List comments = articles.get(0).getComments(); + Assert.assertEquals(1, resolver.getResolved().get(COMMENTS_REL_LINK).intValue()); + Assert.assertEquals(0, resolver.getResolved().get(ARTICLES_AUTHOR_REL_LINK).intValue()); + + // Comments resolution strategy used objects + Assert.assertNotNull(comments); + Assert.assertEquals(2, comments.size()); + Assert.assertEquals(COMMENT_1_ID, comments.get(0).getId()); + Assert.assertEquals(COMMENT_1_BODY, comments.get(0).getBody()); + Assert.assertEquals(COMMENT_2_ID, comments.get(1).getId()); + Assert.assertEquals(COMMENT_2_BODY, comments.get(1).getBody()); + + // Author resolution strategy used references + Assert.assertEquals(ARTICLES_AUTHOR_REL_LINK, articles.get(0).getAuthor()); + Assert.assertEquals(COMMENT_1_AUTHOR_REL_LINK, comments.get(0).getAuthor()); + Assert.assertEquals(COMMENT_2_AUTHOR_REL_LINK, comments.get(1).getAuthor()); + } + + /** + * The {@code ResolutionStrategy#REF reference resolution strategy} requires that the field be a String. + * + * @throws Exception + */ + @Test(expected = IllegalArgumentException.class) + public void testNonStringRefField() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); + ResourceConverter underTest = new ResourceConverter(mapper, Foo.class); + underTest.setGlobalResolver(new ProbeResolver(Collections.emptyMap())); + + underTest.readDocument(org.apache.commons.io.IOUtils.toString( + this.getClass().getResource("foo.json"), "UTF-8").getBytes(), Foo.class); + } +} diff --git a/src/test/resources/com/github/jasminb/jsonapi/resolutionstrategy/articles.json b/src/test/resources/com/github/jasminb/jsonapi/resolutionstrategy/articles.json new file mode 100644 index 0000000..2514894 --- /dev/null +++ b/src/test/resources/com/github/jasminb/jsonapi/resolutionstrategy/articles.json @@ -0,0 +1,31 @@ +{ + "data": [{ + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON API paints my bikeshed!" + }, + "links": { + "self": "http://example.com/articles/1" + }, + "relationships": { + "author": { + "links": { + "related": "http://example.com/articles/1/author" + } + }, + "comments": { + "links": { + "related": "http://example.com/articles/1/comments" + }, + "data": [{ + "type": "comments", + "id": "5" + }, { + "type": "comments", + "id": "12" + }] + } + } + }] +} \ No newline at end of file diff --git a/src/test/resources/com/github/jasminb/jsonapi/resolutionstrategy/comments-response.json b/src/test/resources/com/github/jasminb/jsonapi/resolutionstrategy/comments-response.json new file mode 100644 index 0000000..8429c7a --- /dev/null +++ b/src/test/resources/com/github/jasminb/jsonapi/resolutionstrategy/comments-response.json @@ -0,0 +1,38 @@ +{ + "data": [ + { + "type": "comments", + "id": "5", + "attributes": { + "body": "First!" + }, + "relationships": { + "author": { + "links": { + "related": "http://example.com/comments/5/author" + } + } + }, + "links": { + "self": "http://example.com/comments/5" + } + }, + { + "type": "comments", + "id": "12", + "attributes": { + "body": "I like XML better" + }, + "relationships": { + "author": { + "links": { + "related": "http://example.com/comments/12/author" + } + } + }, + "links": { + "self": "http://example.com/comments/12" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/com/github/jasminb/jsonapi/resolutionstrategy/foo.json b/src/test/resources/com/github/jasminb/jsonapi/resolutionstrategy/foo.json new file mode 100644 index 0000000..69024b6 --- /dev/null +++ b/src/test/resources/com/github/jasminb/jsonapi/resolutionstrategy/foo.json @@ -0,0 +1,16 @@ +{ + "data": { + "type": "foo", + "id": "1", + "attributes": { + "name": "foo" + }, + "relationships": { + "bar": { + "links": { + "self": "http://example.com/foo/rels/bar" + } + } + } + } +} \ No newline at end of file