Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
<scope>test</scope>
</dependency>

</dependencies>

<distributionManagement>
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/com/github/jasminb/jsonapi/ErrorUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.jasminb.jsonapi.models.errors.Error;
import com.github.jasminb.jsonapi.models.errors.ErrorResponse;
import okhttp3.ResponseBody;

Expand Down Expand Up @@ -45,4 +46,38 @@ public static ErrorResponse parseError(JsonNode errorResponse) throws JsonProces
return MAPPER.treeToValue(errorResponse, ErrorResponse.class);
}

/**
* Returns {@code true} if the candidate node contains an 'errors' member. Nodes that contain an 'errors' member
* may be processed by the {@code ErrorUtils} class.
*
* @param candidate a {@code JsonNode} that may or may not contain an 'errors' member.
* @return true if the candidate node contains an 'errors' member, false otherwise.
*/
public static boolean hasErrors(JsonNode candidate) {
return candidate != null && candidate.has(JSONAPISpecConstants.ERRORS);
}

/**
* Populates the supplied {@code StringBuilder} with the contents of the {@code errorResponse}.
*
* @param errorResponse the error
* @param errMessage the {@code StringBuilder} to fill
* @return the same {@code StringBuilder} instance, filled with the error message
*/
public static StringBuilder fill(ErrorResponse errorResponse, StringBuilder errMessage) {
for (Error e : errorResponse.getErrors()) {
if (e.getTitle() != null) {
errMessage.append(e.getTitle()).append(": ");
}
if (e.getCode() != null) {
errMessage.append("Error code: ").append(e.getCode()).append(" ");
}
if (e.getDetail() != null) {
errMessage.append("Detail: ").append(e.getDetail());
}
}

return errMessage;
}

}
39 changes: 35 additions & 4 deletions src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.github.jasminb.jsonapi.annotations.Relationship;
import com.github.jasminb.jsonapi.annotations.Type;
import com.github.jasminb.jsonapi.models.errors.Error;
import com.github.jasminb.jsonapi.models.errors.ErrorResponse;

import java.io.IOException;
import java.lang.reflect.Field;
Expand Down Expand Up @@ -373,11 +376,35 @@ private void handleRelationships(JsonNode source, Object object)
String link;

if (linkNode != null && ((link = getLink(linkNode)) != null)) {
if (isCollection(relationship)) {
relationshipField.set(object,
readDocumentCollection(resolver.resolve(link), type).get());
link = getLink(linkNode);
byte[] linkContent = resolver.resolve(link);

if (hasResourceLinkage(relationship)) {
// If a resource linkage is available, we can determine the cardinality, and call
// the appropriate method: read a collection or read an object
if (isCollection(relationship)) {
relationshipField.set(object, readDocumentCollection(linkContent, type).get());
} else {
relationshipField.set(object, readDocument(linkContent, type).get());
}
} else {
relationshipField.set(object, readDocument(resolver.resolve(link), type).get());
// If the resource linkage is not available, we have to inspect the content of the
// resolved link before making the determination.
JsonNode resolvedNode = objectMapper.readTree(linkContent);
if (ValidationUtils.isCollection(resolvedNode)) {
relationshipField.set(object, readDocumentCollection(linkContent, type).get());
} else if (ValidationUtils.isObject(resolvedNode)) {
relationshipField.set(object, readDocument(linkContent, type).get());
} else if (ErrorUtils.hasErrors(resolvedNode)){
ErrorResponse errors = ErrorUtils.parseError(resolvedNode);
throw new RuntimeException(
ErrorUtils.fill(errors,
new StringBuilder("Unable to parse the response document for " +
"'" + link + "':")).toString());
} else {
throw new RuntimeException("Response document for '" + link + "' does not contain" +
" primary data.");
}
}
}
} else {
Expand Down Expand Up @@ -499,6 +526,10 @@ private boolean isCollection(JsonNode source) {
return data != null && data.isArray();
}

private boolean hasResourceLinkage(JsonNode relationshipObj) {
return relationshipObj.has(DATA);
}


/**
* Converts input object to byte array.
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/com/github/jasminb/jsonapi/ValidationUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ public static void ensureCollection(JsonNode resource) {
}
}

/**
* Convenience method which forwards to {@link #ensureCollection(JsonNode)} in a try/catch.
*
* @param resource resource
* @return true if the provided resource has the required 'data' node and the 'data' node holds an array object;
* {@code false} otherwise.
*/
public static boolean isCollection(JsonNode resource) {
try {
ensureCollection(resource);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}

/**
* Asserts that provided resource has required 'data' node and that node is of type object.
* @param resource resource
Expand All @@ -36,6 +52,22 @@ public static void ensureObject(JsonNode resource) {
}
}

/**
* Convenience method which forwards to {@link #ensureObject(JsonNode)} in a try/catch.
*
* @param resource resource
* @return true if the provided resource has the required 'data' node and the 'data' node is of type object;
* {@code false} otherwise.
*/
public static boolean isObject(JsonNode resource) {
try {
ensureObject(resource);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}

/**
* Returns <code>true</code> in case 'DATA' note has 'ID' and 'TYPE' attributes.
* @param dataNode relationship data node
Expand Down
64 changes: 64 additions & 0 deletions src/test/java/com/github/jasminb/jsonapi/ProbeResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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<String, String> responseMap;

/**
* Map of relationship to a count of the times they have been resolved
*/
private Map<String, Integer> 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<String, String> responseMap) {
this.responseMap = responseMap;
}

/**
* {@inheritDoc}
* <p>
* If the supplied {@code relationshipURL} is missing from the response Map, then an
* {@code IllegalArgumentException} is thrown.
* </p>
* @param relationshipURL URL. eg. <code>users/1</code> or <code>https://api.myhost.com/uers/1</code>
* @return
* @throws IllegalArgumentException if {@code relationshipURL} is missing from the response Map.
*/
@Override
public byte[] resolve(String relationshipURL) {
if (responseMap.containsKey(relationshipURL)) {
if (resolved.containsKey(relationshipURL)) {
int count = resolved.get(relationshipURL);
resolved.put(relationshipURL, ++count);
} else {
resolved.put(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<String, Integer> getResolved() {
return resolved;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.NullNode;
import com.github.jasminb.jsonapi.exceptions.ResourceParseException;
import com.github.jasminb.jsonapi.models.errors.Error;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
Expand All @@ -26,30 +27,35 @@ public void setup() {
@Test(expected = IllegalArgumentException.class)
public void testExpectCollection() throws IOException {
JsonNode node = mapper.readTree(IOUtils.getResourceAsString("user-with-statuses.json"));
Assert.assertFalse(ValidationUtils.isCollection(node));
ValidationUtils.ensureCollection(node);
}

@Test(expected = IllegalArgumentException.class)
public void testExpectObject() throws IOException {
JsonNode node = mapper.readTree(IOUtils.getResourceAsString("users.json"));
Assert.assertFalse(ValidationUtils.isObject(node));
ValidationUtils.ensureObject(node);
}

@Test(expected = IllegalArgumentException.class)
public void testExpectData() throws IOException {
JsonNode node = mapper.readTree("{}");
Assert.assertFalse(ValidationUtils.isCollection(node));
ValidationUtils.ensureCollection(node);
}

@Test(expected = IllegalArgumentException.class)
public void testDataNodeMustBeAnObject() throws IOException {
JsonNode node = mapper.readTree("{\"data\" : \"attribute\"}");
Assert.assertFalse(ValidationUtils.isCollection(node));
ValidationUtils.ensureCollection(node);
}

@Test(expected = ResourceParseException.class)
public void testNodeIsError() throws IOException {
JsonNode node = mapper.readTree(IOUtils.getResourceAsString("errors.json"));
Assert.assertTrue(ErrorUtils.hasErrors(node));
ValidationUtils.ensureNotError(node);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.github.jasminb.jsonapi.models.collectionparsing;


import com.github.jasminb.jsonapi.RelType;
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)
private Author author;

@Relationship(value = "comments", resolve = true, relType = RelType.RELATED)
private List<Comment> 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 Author getAuthor() {
return author;
}

public void setAuthor(Author author) {
this.author = author;
}

public List<Comment> getComments() {
return comments;
}

public void setComments(List<Comment> comments) {
this.comments = comments;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.github.jasminb.jsonapi.models.collectionparsing;


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;
}
}
Loading