diff --git a/src/main/java/com/github/jasminb/jsonapi/JSONAPISpecConstants.java b/src/main/java/com/github/jasminb/jsonapi/JSONAPISpecConstants.java
index ee42f13..7c0d4e0 100644
--- a/src/main/java/com/github/jasminb/jsonapi/JSONAPISpecConstants.java
+++ b/src/main/java/com/github/jasminb/jsonapi/JSONAPISpecConstants.java
@@ -18,4 +18,8 @@ public interface JSONAPISpecConstants {
String ERRORS = "errors";
String META = "meta";
String HREF = "href";
+ String PREV = "prev";
+ String NEXT = "next";
+ String FIRST = "first";
+ String LAST = "last";
}
diff --git a/src/main/java/com/github/jasminb/jsonapi/Link.java b/src/main/java/com/github/jasminb/jsonapi/Link.java
new file mode 100644
index 0000000..c0217ae
--- /dev/null
+++ b/src/main/java/com/github/jasminb/jsonapi/Link.java
@@ -0,0 +1,44 @@
+package com.github.jasminb.jsonapi;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Models a JSON API Link object.
+ */
+public class Link {
+
+ private String href;
+
+ private Map meta = Collections.emptyMap();
+
+ public Link() {
+
+ }
+
+ public Link(String href) {
+ this.href = href;
+ }
+
+ public Link(String href, Map meta) {
+ this.href = href;
+ this.meta = meta;
+ }
+
+ public String getHref() {
+ return href;
+ }
+
+ public void setHref(String href) {
+ this.href = href;
+ }
+
+ public Map getMeta() {
+ return meta;
+ }
+
+ public void setMeta(Map meta) {
+ this.meta = meta;
+ }
+
+}
diff --git a/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java b/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java
index dfdab28..a89560b 100644
--- a/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java
+++ b/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java
@@ -1,11 +1,16 @@
package com.github.jasminb.jsonapi;
import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.type.MapType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
import com.github.jasminb.jsonapi.annotations.Id;
import com.github.jasminb.jsonapi.annotations.Meta;
import com.github.jasminb.jsonapi.annotations.Relationship;
@@ -198,7 +203,7 @@ public T readObject(byte [] data, Class clazz) {
* @return collection of converted elements
* @throws RuntimeException in case conversion fails
*/
- public List readObjectCollection(byte [] data, Class clazz) {
+ public ResourceList readObjectCollection(byte [] data, Class clazz) {
try {
JsonNode rootNode = objectMapper.readTree(data);
@@ -216,14 +221,25 @@ public List readObjectCollection(byte [] data, Class clazz) {
result.add(pojo);
}
- return result;
+ ResourceList wrapper = new ResourceList<>(result);
+
+ if (rootNode.has(LINKS)) {
+ Map links = mapLinks(rootNode.get(LINKS));
+ wrapper.setLinks(links);
+ }
+
+ if (rootNode.has(META)) {
+ Map meta = mapMeta(rootNode.get(META));
+ wrapper.setMeta(meta);
+ }
+
+ return wrapper;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
-
}
/**
@@ -394,6 +410,62 @@ private void handleRelationships(JsonNode source, Object object, MapJSON-API links object to a {@code Map}
+ * keyed by the link name.
+ *
+ * The {@code linksObject} may represent links in string form or object form; both are supported by this method.
+ *
+ *
+ * E.g.
+ *
+ * "links": {
+ * "self": "http://example.com/posts"
+ * }
+ *
+ *
+ *
+ * or
+ *
+ * "links": {
+ * "related": {
+ * "href": "http://example.com/articles/1/comments",
+ * "meta": {
+ * "count": 10
+ * }
+ * }
+ * }
+ *
+ *
+ *
+ * @param linksObject a {@code JsonNode} representing a links object
+ * @return a {@code Map} keyed by link name
+ */
+ private Map mapLinks(JsonNode linksObject) {
+ Map result = new HashMap<>();
+
+ Iterator> linkItr = linksObject.fields();
+
+ while (linkItr.hasNext()) {
+ Map.Entry linkNode = linkItr.next();
+ Link linkObj = new Link();
+
+ linkObj.setHref(
+ getLink(
+ linkNode.getValue()));
+
+ if (linkNode.getValue().has(META)) {
+ linkObj.setMeta(
+ mapMeta(
+ linkNode.getValue().get(META)));
+ }
+
+ result.put(linkNode.getKey(), linkObj);
+ }
+
+ return result;
+ }
+
/**
* Accepts a JsonNode which encapsulates a link. The link may be represented as a simple string or as
* link object. This method introspects on the
@@ -413,6 +485,27 @@ private String getLink(JsonNode linkNode) {
return linkNode.asText();
}
+ /**
+ * Deserializes a JSON-API meta object to a {@code Map}
+ * keyed by the member names. Because {@code meta} objects contain arbitrary information, the values in the
+ * map are of unknown type.
+ *
+ * @param metaNode a JsonNode representing a meta object
+ * @return a Map of the meta information, keyed by member name.
+ */
+ private Map mapMeta(JsonNode metaNode) {
+ JsonParser p = objectMapper.treeAsTokens(metaNode);
+ MapType mapType = TypeFactory.defaultInstance()
+ .constructMapType(HashMap.class, String.class, Object.class);
+ try {
+ return objectMapper.readValue(p, mapType);
+ } catch (IOException e) {
+ // TODO: log? No recovery.
+ }
+
+ return null;
+ }
+
/**
* Creates relationship object by consuming provided 'data' node.
* @param relationshipDataNode relationship data node
diff --git a/src/main/java/com/github/jasminb/jsonapi/ResourceList.java b/src/main/java/com/github/jasminb/jsonapi/ResourceList.java
new file mode 100644
index 0000000..24226dd
--- /dev/null
+++ b/src/main/java/com/github/jasminb/jsonapi/ResourceList.java
@@ -0,0 +1,292 @@
+package com.github.jasminb.jsonapi;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import static com.github.jasminb.jsonapi.JSONAPISpecConstants.FIRST;
+import static com.github.jasminb.jsonapi.JSONAPISpecConstants.LAST;
+import static com.github.jasminb.jsonapi.JSONAPISpecConstants.NEXT;
+import static com.github.jasminb.jsonapi.JSONAPISpecConstants.PREV;
+import static com.github.jasminb.jsonapi.JSONAPISpecConstants.RELATED;
+import static com.github.jasminb.jsonapi.JSONAPISpecConstants.SELF;
+
+/**
+ * Encapsulates a JSON API response that includes a collection of resource objects, with optional links and meta
+ * objects.
+ *
+ * JSON API calls that return a collection of resource objects may be paginated or may contain meta information
+ * describing the response. This implementation exposes this information by providing access to a {@code Map} of
+ * {@link #getLinks() links}, and a {@code Map} of {@link #getMeta() meta information}. Convenience methods exist for
+ * common use cases like pagination links, and obtaining
+ * the {@link #getRelated() related} and {@link #getSelf() self} relation types.
+ *
+ *
+ * Implementation note: any {@code List} operations are forwarded to the enclosed
+ * {@link #ResourceList(List) resource list} supplied on construction.
+ *
+ */
+public class ResourceList implements List {
+
+ /**
+ * A map of link objects keyed by link name.
+ */
+ private Map links = Collections.emptyMap();
+
+ /**
+ * A map of meta fields, keyed by the meta field name
+ */
+ private Map meta = Collections.emptyMap();
+
+ /**
+ * A list of resource objects, the primary data of JSON API response
+ */
+ private List resources = Collections.emptyList();
+
+ /**
+ * Constructs a new resource list composed of the underlying list of resources.
+ *
+ * @param resources a list of resource objects representing the primary data in the JSON API response;
+ * must not be {@code null}
+ */
+ public ResourceList(List resources) {
+ if (resources == null) {
+ throw new IllegalArgumentException("Resources list must not be null.");
+ }
+ this.resources = resources;
+ }
+
+ /**
+ * Convenience method for returning the value of the {@code prev} link.
+ *
+ * @return the link value, or {@code null} if the named link does not exist or has no value
+ */
+ public String getPrevious() {
+ return getLink(PREV);
+ }
+
+ /**
+ * Convenience method for returning the value of the {@code first} link.
+ *
+ * @return the link value, or {@code null} if the named link does not exist or has no value
+ */
+ public String getFirst() {
+ return getLink(FIRST);
+ }
+
+ /**
+ * Convenience method for returning the value of the {@code next} link.
+ *
+ * @return the link value, or {@code null} if the named link does not exist or has no value
+ */
+ public String getNext() {
+ return getLink(NEXT);
+ }
+
+ /**
+ * Convenience method for returning the value of the {@code last} link.
+ *
+ * @return the link value, or {@code null} if the named link does not exist or has no value
+ */
+ public String getLast() {
+ return getLink(LAST);
+ }
+
+ /**
+ * Convenience method for returning the value of the {@code self} link.
+ *
+ * @return the link value, or {@code null} if the named link does not exist or has no value
+ */
+ public String getSelf() {
+ return getLink(SELF);
+ }
+
+ /**
+ * Convenience method for returning the value of the {@code related} link.
+ *
+ * @return the link value, or {@code null} if the named link does not exist or has no value
+ */
+ public String getRelated() {
+ return getLink(RELATED);
+ }
+
+ /**
+ * Returns the JSON API links present in the
+ * response.
+ *
+ * @return the links in the response keyed by link name; may be empty but never {@code null}
+ */
+ public Map getLinks() {
+ return links;
+ }
+
+ /**
+ * Convenience method for returning the value of the named link.
+ *
+ * @return the link value, or {@code null} if the named link does not exist or has no value
+ */
+ public String getLink(String linkName) {
+ if (links.containsKey(linkName)) {
+ return links.get(linkName).getHref();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the JSON API meta information present in the
+ * response. Because meta information can contain arbitrary data, the values in the returned {@code Map} are of
+ * unknown type.
+ *
+ * @return the meta information in the response keyed by field name; may be empty but never {@code null}
+ */
+ public Map getMeta() {
+ return meta;
+ }
+
+ /**
+ * Package-private method for setting the meta information.
+ *
+ * @param meta the meta information, must not be {@code null}
+ */
+ void setMeta(Map meta) {
+ if (meta == null) {
+ throw new IllegalArgumentException("Meta information must not be null.");
+ }
+ this.meta = meta;
+ }
+
+ /**
+ * Package-private method for setting link information.
+ *
+ * @param links the links information, must not be {@code null}
+ */
+ void setLinks(Map links) {
+ if (links == null) {
+ throw new IllegalArgumentException("Links map must not be null.");
+ }
+ this.links = links;
+ }
+
+ /*
+ * List implementation: all methods for the List interface forward to the member {@link #resources resources list}.
+ */
+
+ @Override
+ public int size() {
+ return resources.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return resources.isEmpty();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return resources.contains(o);
+ }
+
+ @Override
+ public Iterator iterator() {
+ return resources.iterator();
+ }
+
+ @Override
+ public Object[] toArray() {
+ return resources.toArray();
+ }
+
+ @Override
+ public T[] toArray(T[] a) {
+ return resources.toArray(a);
+ }
+
+ @Override
+ public boolean add(E e) {
+ return resources.add(e);
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ return resources.remove(o);
+ }
+
+ @Override
+ public boolean containsAll(Collection> c) {
+ return resources.containsAll(c);
+ }
+
+ @Override
+ public boolean addAll(Collection extends E> c) {
+ return resources.addAll(c);
+ }
+
+ @Override
+ public boolean addAll(int index, Collection extends E> c) {
+ return resources.addAll(index, c);
+ }
+
+ @Override
+ public boolean removeAll(Collection> c) {
+ return resources.removeAll(c);
+ }
+
+ @Override
+ public boolean retainAll(Collection> c) {
+ return resources.retainAll(c);
+ }
+
+ @Override
+ public void clear() {
+ resources.clear();
+ }
+
+ @Override
+ public E get(int index) {
+ return resources.get(index);
+ }
+
+ @Override
+ public E set(int index, E element) {
+ return resources.set(index, element);
+ }
+
+ @Override
+ public void add(int index, E element) {
+ resources.add(index, element);
+ }
+
+ @Override
+ public E remove(int index) {
+ return resources.remove(index);
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ return resources.indexOf(o);
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ return resources.lastIndexOf(o);
+ }
+
+ @Override
+ public ListIterator listIterator() {
+ return resources.listIterator();
+ }
+
+ @Override
+ public ListIterator listIterator(int index) {
+ return resources.listIterator(index);
+ }
+
+ @Override
+ public List subList(int fromIndex, int toIndex) {
+ return resources.subList(fromIndex, toIndex);
+ }
+}
diff --git a/src/test/java/com/github/jasminb/jsonapi/ResourceListTest.java b/src/test/java/com/github/jasminb/jsonapi/ResourceListTest.java
new file mode 100644
index 0000000..8f3d163
--- /dev/null
+++ b/src/test/java/com/github/jasminb/jsonapi/ResourceListTest.java
@@ -0,0 +1,148 @@
+package com.github.jasminb.jsonapi;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+
+/**
+ * Insures that the {@code ResourceList} behavior is in accordance with its Javadoc.
+ */
+public class ResourceListTest {
+
+ private final ResourceList EMPTY_RESOURCE_LIST = new ResourceList<>(Collections.emptyList());
+
+ private final Link LINK_NO_META = new Link("http://example.com/link/rel");
+
+ @Before
+ public void setUp() throws Exception {
+ Assert.assertTrue(EMPTY_RESOURCE_LIST.size() == 0);
+ Assert.assertTrue(EMPTY_RESOURCE_LIST.getLinks().isEmpty());
+ Assert.assertTrue(EMPTY_RESOURCE_LIST.getMeta().isEmpty());
+
+ Assert.assertTrue(LINK_NO_META.getMeta().size() == 0);
+ Assert.assertEquals("http://example.com/link/rel", LINK_NO_META.getHref());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullityLinksSetter() throws Exception {
+ EMPTY_RESOURCE_LIST.setLinks(null);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullityMetaSetter() throws Exception {
+ EMPTY_RESOURCE_LIST.setMeta(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullityConstructor() throws Exception {
+ new ResourceList<>(null);
+ }
+
+ @Test
+ public void testGetLink() throws Exception {
+ Assert.assertNull(EMPTY_RESOURCE_LIST.getLink("foo"));
+
+ EMPTY_RESOURCE_LIST.setLinks(new HashMap() {
+ {
+ put("foo", LINK_NO_META);
+ }
+ });
+
+ Assert.assertNotNull(EMPTY_RESOURCE_LIST.getLink("foo"));
+ }
+
+ @Test
+ public void testGetPrev() throws Exception {
+ Assert.assertNull(EMPTY_RESOURCE_LIST.getPrevious());
+
+ EMPTY_RESOURCE_LIST.setLinks(new HashMap() {
+ {
+ put(JSONAPISpecConstants.PREV, LINK_NO_META);
+ }
+ });
+
+ Assert.assertNotNull(EMPTY_RESOURCE_LIST.getPrevious());
+ }
+
+ @Test
+ public void testGetNext() throws Exception {
+ Assert.assertNull(EMPTY_RESOURCE_LIST.getNext());
+
+ EMPTY_RESOURCE_LIST.setLinks(new HashMap() {
+ {
+ put(JSONAPISpecConstants.NEXT, LINK_NO_META);
+ }
+ });
+
+ Assert.assertNotNull(EMPTY_RESOURCE_LIST.getNext());
+ }
+
+ @Test
+ public void testGetFirst() throws Exception {
+ Assert.assertNull(EMPTY_RESOURCE_LIST.getFirst());
+
+ EMPTY_RESOURCE_LIST.setLinks(new HashMap() {
+ {
+ put(JSONAPISpecConstants.FIRST, LINK_NO_META);
+ }
+ });
+
+ Assert.assertNotNull(EMPTY_RESOURCE_LIST.getFirst());
+ }
+
+ @Test
+ public void testGetLast() throws Exception {
+ Assert.assertNull(EMPTY_RESOURCE_LIST.getLast());
+
+ EMPTY_RESOURCE_LIST.setLinks(new HashMap() {
+ {
+ put(JSONAPISpecConstants.LAST, LINK_NO_META);
+ }
+ });
+
+ Assert.assertNotNull(EMPTY_RESOURCE_LIST.getLast());
+ }
+
+ @Test
+ public void testGetSelf() throws Exception {
+ Assert.assertNull(EMPTY_RESOURCE_LIST.getSelf());
+
+ EMPTY_RESOURCE_LIST.setLinks(new HashMap() {
+ {
+ put(JSONAPISpecConstants.SELF, LINK_NO_META);
+ }
+ });
+
+ Assert.assertNotNull(EMPTY_RESOURCE_LIST.getSelf());
+ }
+
+ @Test
+ public void testGetRelated() throws Exception {
+ Assert.assertNull(EMPTY_RESOURCE_LIST.getRelated());
+
+ EMPTY_RESOURCE_LIST.setLinks(new HashMap() {
+ {
+ put(JSONAPISpecConstants.RELATED, LINK_NO_META);
+ }
+ });
+
+ Assert.assertNotNull(EMPTY_RESOURCE_LIST.getRelated());
+ }
+
+ @Test
+ public void testGetMeta() throws Exception {
+ Assert.assertEquals(0, EMPTY_RESOURCE_LIST.getMeta().size());
+
+ EMPTY_RESOURCE_LIST.setMeta(new HashMap() {
+ {
+ put("total_results", 10);
+ }
+ });
+
+ Assert.assertEquals(1, EMPTY_RESOURCE_LIST.getMeta().size());
+ }
+}
\ No newline at end of file