com.github.jasminb.jsonapi.ResourceConverter Maven / Gradle / Ivy
Show all versions of jsonapi-converter Show documentation
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.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.Relationship;
import com.github.jasminb.jsonapi.annotations.Type;
import com.github.jasminb.jsonapi.exceptions.DocumentSerializationException;
import com.github.jasminb.jsonapi.models.errors.Error;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.github.jasminb.jsonapi.JSONAPISpecConstants.*;
/**
* JSON API data converter.
*
* Provides methods for conversion between JSON API resources to java POJOs and vice versa.
*
* @author jbegic
*/
public class ResourceConverter {
private final ConverterConfiguration configuration;
private final ObjectMapper objectMapper;
private final Map, RelationshipResolver> typedResolvers = new HashMap<>();
private final ResourceCache resourceCache;
private final Set deserializationFeatures = DeserializationFeature.getDefaultFeatures();
private final Set serializationFeatures = SerializationFeature.getDefaultFeatures();
private RelationshipResolver globalResolver;
private String baseURL;
/**
* Creates new ResourceConverter.
*
* All classes that should be handled by instance of {@link ResourceConverter} must be registered
* when creating a new instance of it.
*
* @param classes {@link Class} array of classes to be handled by this resource converter instance
*/
public ResourceConverter(Class>... classes) {
this(null, null, classes);
}
/**
* Creates new ResourceConverter.
*
* All classes that should be handled by instance of {@link ResourceConverter} must be registered
* when creating a new instance of it.
*
* @param baseURL {@link String} base URL, eg. https://api.mysite.com
* @param classes {@link Class} array of classes to be handled by this resource converter instance
*/
public ResourceConverter(String baseURL, Class>... classes) {
this(null, baseURL, classes);
}
public ResourceConverter(ObjectMapper mapper, Class>... classes) {
this(mapper, null, classes);
}
/**
* Creates new ResourceConverter.
* @param mapper {@link ObjectMapper} custom mapper to be used for resource parsing
* @param baseURL {@link String} base URL, eg. https://api.mysite.com
* @param classes {@link Class} array of classes to be handled by this resource converter instance
*/
public ResourceConverter(ObjectMapper mapper, String baseURL, Class>... classes) {
this.configuration = new ConverterConfiguration(classes);
this.baseURL = baseURL != null ? baseURL : "";
// Set custom mapper if provided
if (mapper != null) {
objectMapper = mapper;
} else {
objectMapper = new ObjectMapper();
}
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
resourceCache = new ResourceCache();
}
/**
* Registers global relationship resolver. This resolver will be used in case relationship is present in the
* API response but not provided in the included
section and relationship resolving is enabled
* trough relationship annotation.
* In case type resolver is registered it will be used instead.
* @param resolver resolver instance
*/
public void setGlobalResolver(RelationshipResolver resolver) {
this.globalResolver = resolver;
}
/**
* Registers relationship resolver for given type. Resolver will be used if relationship resolution is enabled
* trough relationship annotation.
* @param resolver resolver instance
* @param type type
*/
public void setTypeResolver(RelationshipResolver resolver, Class> type) {
if (resolver != null) {
String typeName = ReflectionUtils.getTypeName(type);
if (typeName != null) {
typedResolvers.put(type, resolver);
}
}
}
/**
* Converts raw data input into requested target type.
* @param data raw data
* @param clazz target object
* @param type
* @return converted object
* @throws RuntimeException in case conversion fails
*/
@Deprecated
public T readObject(byte [] data, Class clazz) {
return readDocument(data, clazz).get();
}
/**
* Converts rawdata input into a collection of requested output objects.
* @param data raw data input
* @param clazz target type
* @param type
* @return collection of converted elements
* @throws RuntimeException in case conversion fails
*/
@Deprecated
public List readObjectCollection(byte [] data, Class clazz) {
return readDocumentCollection(data, clazz).get();
}
/**
* Reads JSON API spec document and converts it into target type.
* @param data {@link byte} raw data (server response)
* @param clazz {@link Class} target type
* @param type
* @return {@link JSONAPIDocument}
*/
public JSONAPIDocument readDocument(byte[] data, Class clazz) {
return readDocument(new ByteArrayInputStream(data), clazz);
}
/**
* Reads JSON API spec document and converts it into target type.
* @param dataStream {@link byte} raw dataStream (server response)
* @param clazz {@link Class} target type
* @param type
* @return {@link JSONAPIDocument}
*/
public JSONAPIDocument readDocument(InputStream dataStream, Class clazz) {
try {
resourceCache.init();
JsonNode rootNode = objectMapper.readTree(dataStream);
// Validate
ValidationUtils.ensureNotError(objectMapper, rootNode);
ValidationUtils.ensureValidResource(rootNode);
resourceCache.cache(parseIncluded(rootNode));
JsonNode dataNode = rootNode.get(DATA);
JSONAPIDocument result;
if (dataNode != null && dataNode.isObject()) {
T resourceObject = readObject(dataNode, clazz, true);
result = new JSONAPIDocument<>(resourceObject, objectMapper);
} else {
result = new JSONAPIDocument<>(null, objectMapper);
}
// Handle top-level meta
if (rootNode.has(META)) {
result.setMeta(mapMeta(rootNode.get(META)));
}
// Handle top-level links
if (rootNode.has(LINKS)) {
result.setLinks(new Links(mapLinks(rootNode.get(LINKS))));
}
return result;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
resourceCache.clear();
}
}
/**
* Reads JSON API spec document and converts it into collection of target type objects.
* @param data {@link byte} raw data (server response)
* @param clazz {@link Class} target type
* @param type
* @return {@link JSONAPIDocument}
*/
public JSONAPIDocument> readDocumentCollection(byte[] data, Class clazz) {
return readDocumentCollection(new ByteArrayInputStream(data), clazz);
}
/**
* Reads JSON API spec document and converts it into collection of target type objects.
* @param dataStream {@link InputStream} input stream
* @param clazz {@link Class} target type
* @param type
* @return {@link JSONAPIDocument}
*/
public JSONAPIDocument> readDocumentCollection(InputStream dataStream, Class clazz) {
try {
resourceCache.init();
JsonNode rootNode = objectMapper.readTree(dataStream);
// Validate
ValidationUtils.ensureNotError(objectMapper, rootNode);
ValidationUtils.ensureValidResource(rootNode);
resourceCache.cache(parseIncluded(rootNode));
List resourceList = new ArrayList<>();
if (rootNode.has(DATA) && rootNode.get(DATA).isArray()) {
for (JsonNode element : rootNode.get(DATA)) {
T pojo = readObject(element, clazz, true);
resourceList.add(pojo);
}
}
JSONAPIDocument> result = new JSONAPIDocument<>(resourceList, objectMapper);
// Handle top-level meta
if (rootNode.has(META)) {
result.setMeta(mapMeta(rootNode.get(META)));
}
// Handle top-level links
if (rootNode.has(LINKS)) {
result.setLinks(new Links(mapLinks(rootNode.get(LINKS))));
}
return result;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
resourceCache.clear();
}
}
/**
* Converts provided input into a target object. After conversion completes any relationships defined are resolved.
* @param source JSON source
* @param clazz target type
* @param type
* @return converted target object
* @throws IOException
* @throws IllegalAccessException
*/
private T readObject(JsonNode source, Class clazz, boolean handleRelationships)
throws IOException, IllegalAccessException, InstantiationException {
String identifier = createIdentifier(source);
T result = (T) resourceCache.get(identifier);
if (result == null) {
Class> type = getActualType(source, clazz);
if (source.has(ATTRIBUTES)) {
result = (T) objectMapper.treeToValue(source.get(ATTRIBUTES), type);
} else {
if (type.isInterface()) {
result = null;
} else {
result = (T) objectMapper.treeToValue(objectMapper.createObjectNode(), type);
}
}
// Handle meta
if (source.has(META)) {
Field field = configuration.getMetaField(type);
if (field != null) {
Class> metaType = configuration.getMetaType(type);
Object metaObject = objectMapper.treeToValue(source.get(META), metaType);
field.set(result, metaObject);
}
}
// Handle links
if (source.has(LINKS)) {
Field linkField = configuration.getLinksField(type);
if (linkField != null) {
linkField.set(result, new Links(mapLinks(source.get(LINKS))));
}
}
if (result != null) {
// Add parsed object to cache
resourceCache.cache(identifier, result);
// Set object id
setIdValue(result, source.get(ID));
if (handleRelationships) {
// Handle relationships
handleRelationships(source, result);
}
}
}
return result;
}
/**
* Converts included data and returns it as pairs of its unique identifiers and converted types.
* @param parent data source
* @return identifier/object pairs
* @throws IOException
* @throws IllegalAccessException
*/
private Map parseIncluded(JsonNode parent)
throws IOException, IllegalAccessException, InstantiationException {
Map result = new HashMap<>();
if (parent.has(INCLUDED)) {
// Get resources
List includedResources = getIncludedResources(parent);
if (!includedResources.isEmpty()) {
// Add to result
for (Resource includedResource : includedResources) {
result.put(includedResource.getIdentifier(), includedResource.getObject());
}
ArrayNode includedArray = (ArrayNode) parent.get(INCLUDED);
for (int i = 0; i < includedResources.size(); i++) {
Resource resource = includedResources.get(i);
// Handle relationships
JsonNode node = includedArray.get(i);
handleRelationships(node, resource.getObject());
}
}
}
return result;
}
/**
* Parses out included resources excluding relationships.
* @param parent root node
* @return map of identifier/resource pairs
* @throws IOException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private List getIncludedResources(JsonNode parent)
throws IOException, IllegalAccessException, InstantiationException {
List result = new ArrayList<>();
if (parent.has(INCLUDED)) {
for (JsonNode jsonNode : parent.get(INCLUDED)) {
String type = jsonNode.get(TYPE).asText();
Class> clazz = configuration.getTypeClass(type);
if (clazz != null) {
Object object = readObject(jsonNode, clazz, false);
if (object != null) {
result.add(new Resource(createIdentifier(jsonNode), object));
}
} else if (!deserializationFeatures.contains(DeserializationFeature.ALLOW_UNKNOWN_INCLUSIONS)) {
throw new IllegalArgumentException("Included section contains unknown resource type: " + type);
}
}
}
return result;
}
private void handleRelationships(JsonNode source, Object object)
throws IllegalAccessException, IOException, InstantiationException {
JsonNode relationships = source.get(RELATIONSHIPS);
if (relationships != null) {
Iterator fields = relationships.fieldNames();
while (fields.hasNext()) {
String field = fields.next();
JsonNode relationship = relationships.get(field);
Field relationshipField = configuration.getRelationshipField(object.getClass(), field);
if (relationshipField != null) {
// Get target type
Class> type = configuration.getRelationshipType(object.getClass(), field);
// In case type is not defined, relationship object cannot be processed
if (type == null) {
continue;
}
// Handle meta if present
if (relationship.has(META)) {
Field relationshipMetaField = configuration.getRelationshipMetaField(object.getClass(), field);
if (relationshipMetaField != null) {
relationshipMetaField.set(object, objectMapper.treeToValue(relationship.get(META),
configuration.getRelationshipMetaType(object.getClass(), field)));
}
}
// Handle links if present
if (relationship.has(LINKS)) {
Field relationshipLinksField = configuration.getRelationshipLinksField(object.getClass(), field);
if (relationshipLinksField != null) {
Links links = new Links(mapLinks(relationship.get(LINKS)));
relationshipLinksField.set(object, links);
}
}
// Get resolve flag
boolean resolveRelationship = configuration.getFieldRelationship(relationshipField).resolve();
RelationshipResolver resolver = getResolver(type);
// Use resolver if possible
if (resolveRelationship && resolver != null && relationship.has(LINKS)) {
String relType = configuration.getFieldRelationship(relationshipField).relType().getRelName();
JsonNode linkNode = relationship.get(LINKS).get(relType);
String link;
if (linkNode != null && ((link = getLink(linkNode)) != null)) {
if (isCollection(relationship)) {
relationshipField.set(object,
readDocumentCollection(new ByteArrayInputStream(resolver.resolve(link)), type).get());
} else {
relationshipField.set(object, readDocument(new ByteArrayInputStream(resolver.resolve(link)), type).get());
}
}
} else {
if (isCollection(relationship)) {
@SuppressWarnings("rawtypes")
Collection elements = createCollectionInstance(relationshipField.getType());
for (JsonNode element : relationship.get(DATA)) {
Object relationshipObject = parseRelationship(element, type);
if (relationshipObject != null) {
elements.add(relationshipObject);
}
}
relationshipField.set(object, elements);
} else {
Object relationshipObject = parseRelationship(relationship.get(DATA), type);
if (relationshipObject != null) {
relationshipField.set(object, relationshipObject);
}
}
}
}
}
}
}
/**
* 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
* {@code linkNode}, returning the value of the {@code href} member, if it exists, or returns the string form
* of the {@code linkNode} if it doesn't.
*
* Package-private for unit testing.
*
* @param linkNode a JsonNode representing a link, may return {@code null}
* @return the link URL
*/
String getLink(JsonNode linkNode) {
// Handle both representations of a link: as a string or as an object
// http://jsonapi.org/format/#document-links (v1.0)
if (linkNode.has(HREF)) {
// object form
return linkNode.get(HREF).asText();
}
return linkNode.asText(null);
}
/**
* Creates relationship object by consuming provided 'data' node.
* @param relationshipDataNode relationship data node
* @param type object type
* @return created object or null
in case data node is not valid
* @throws IOException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private Object parseRelationship(JsonNode relationshipDataNode, Class> type)
throws IOException, IllegalAccessException, InstantiationException {
if (ValidationUtils.isRelationshipParsable(relationshipDataNode)) {
String identifier = createIdentifier(relationshipDataNode);
if (resourceCache.contains(identifier)) {
return resourceCache.get(identifier);
} else {
// Never cache relationship objects
resourceCache.lock();
try {
return readObject(relationshipDataNode, type, true);
} finally {
resourceCache.unlock();
}
}
}
return null;
}
/**
* Generates unique resource identifier by combining resource type and resource id fields.
* By specification id/type combination guarantees uniqueness.
* @param object data object
* @return concatenated id and type values
*/
private String createIdentifier(JsonNode object) {
JsonNode idNode = object.get(ID);
String id = idNode != null ? idNode.asText().trim() : "";
if (id.isEmpty() && deserializationFeatures.contains(DeserializationFeature.REQUIRE_RESOURCE_ID)) {
throw new IllegalArgumentException("Resource must have an non null and non-empty 'id' attribute!");
}
String type = object.get(TYPE).asText();
return type.concat(id);
}
/**
* Sets an id attribute value to a target object.
* @param target target POJO
* @param idValue id node
* @throws IllegalAccessException thrown in case target field is not accessible
*/
private void setIdValue(Object target, JsonNode idValue) throws IllegalAccessException {
Field idField = configuration.getIdField(target.getClass());
ResourceIdHandler idHandler = configuration.getIdHandler(target.getClass());
if (idValue != null) {
idField.set(target, idHandler.fromString(idValue.asText()));
}
}
/**
* Reads @Id value from provided source object.
*
* @param source object to read @Id value from
* @return {@link String} id or null
* @throws IllegalAccessException
*/
private String getIdValue(Object source) throws IllegalAccessException {
Field idField = configuration.getIdField(source.getClass());
ResourceIdHandler handler = configuration.getIdHandler(source.getClass());
return handler.asString(idField.get(source));
}
/**
* Checks if data
object is an array or just single object holder.
* @param source data node
* @return true
if data node is an array else false
*/
private boolean isCollection(JsonNode source) {
JsonNode data = source.get(DATA);
return data != null && data.isArray();
}
/**
* Converts input object to byte array.
* @param object input object
* @return raw bytes
* @throws JsonProcessingException
* @throws IllegalAccessException
*/
@Deprecated
public byte [] writeObject(Object object) throws JsonProcessingException, IllegalAccessException {
try {
return writeDocument(new JSONAPIDocument<>(object));
} catch (DocumentSerializationException e) {
throw new RuntimeException(e);
}
}
/**
* Serializes provided {@link JSONAPIDocument} into JSON API Spec compatible byte representation.
* @param document {@link JSONAPIDocument} document to serialize
* @return serialized content in bytes
* @throws DocumentSerializationException thrown in case serialization fails
*/
public byte [] writeDocument(JSONAPIDocument> document) throws DocumentSerializationException {
return writeDocument(document, null);
}
/**
* Serializes provided {@link JSONAPIDocument} into JSON API Spec compatible byte representation.
* @param document {@link JSONAPIDocument} document to serialize
* @param settings {@link SerializationSettings} settings that override global serialization settings
* @return serialized content in bytes
* @throws DocumentSerializationException thrown in case serialization fails
*/
public byte [] writeDocument(JSONAPIDocument> document, SerializationSettings settings)
throws DocumentSerializationException {
try {
resourceCache.init();
Map includedDataMap = new HashMap<>();
ObjectNode result = objectMapper.createObjectNode();
// Serialize data if present
if (document.get() != null) {
ObjectNode dataNode = getDataNode(document.get(), includedDataMap, settings);
result.set(DATA, dataNode);
result = addIncludedSection(result, includedDataMap);
}
// Serialize errors if present
if (document.getErrors() != null) {
ArrayNode errorsNode = objectMapper.createArrayNode();
for (Error error : document.getErrors()) {
errorsNode.add(objectMapper.valueToTree(error));
}
result.set(ERRORS, errorsNode);
}
// Serialize global links and meta
serializeMeta(document, result, settings);
serializeLinks(document, result, settings);
return objectMapper.writeValueAsBytes(result);
} catch (Exception e) {
throw new DocumentSerializationException(e);
} finally {
resourceCache.clear();
}
}
private void serializeMeta(JSONAPIDocument> document, ObjectNode resultNode, SerializationSettings settings) {
// Handle global links and meta
if (document.getMeta() != null && !document.getMeta().isEmpty() && shouldSerializeMeta(settings)) {
resultNode.set(META, objectMapper.valueToTree(document.getMeta()));
}
}
private void serializeLinks(JSONAPIDocument> document, ObjectNode resultNode, SerializationSettings settings) {
if (document.getLinks() != null && !document.getLinks().getLinks().isEmpty() &&
shouldSerializeLinks(settings)) {
resultNode.set(LINKS, objectMapper.valueToTree(document.getLinks()).get(LINKS));
}
}
/**
* Serializes provided {@link JSONAPIDocument} into JSON API Spec compatible byte representation.
* @param documentCollection {@link JSONAPIDocument} document collection to serialize
* @return serialized content in bytes
* @throws DocumentSerializationException thrown in case serialization fails
*/
public byte [] writeDocumentCollection(JSONAPIDocument extends Iterable>> documentCollection)
throws DocumentSerializationException {
return writeDocumentCollection(documentCollection, null);
}
/**
* Serializes provided {@link JSONAPIDocument} into JSON API Spec compatible byte representation.
* @param documentCollection {@link JSONAPIDocument} document collection to serialize
* @param serializationSettings {@link SerializationSettings} settings that override global serialization settings
* @return serialized content in bytes
* @throws DocumentSerializationException thrown in case serialization fails
*/
public byte [] writeDocumentCollection(JSONAPIDocument extends Iterable>> documentCollection,
SerializationSettings serializationSettings)
throws DocumentSerializationException {
try {
resourceCache.init();
ArrayNode results = objectMapper.createArrayNode();
Map includedDataMap = new HashMap<>();
for (Object object : documentCollection.get()) {
results.add(getDataNode(object, includedDataMap, serializationSettings));
}
ObjectNode result = objectMapper.createObjectNode();
result.set(DATA, results);
result = addIncludedSection(result, includedDataMap);
// Handle global links and meta
serializeMeta(documentCollection, result, serializationSettings);
serializeLinks(documentCollection, result, serializationSettings);
return objectMapper.writeValueAsBytes(result);
} catch (Exception e) {
throw new DocumentSerializationException(e);
} finally {
resourceCache.clear();
}
}
private ObjectNode getDataNode(Object object, Map includedContainer,
SerializationSettings settings) throws IllegalAccessException {
ObjectNode dataNode = objectMapper.createObjectNode();
// Perform initial conversion
ObjectNode attributesNode = objectMapper.valueToTree(object);
// Handle id, meta and relationship fields
String resourceId = getIdValue(object);
// Remove id field from resulting attribute node
attributesNode.remove(configuration.getIdField(object.getClass()).getName());
// Handle meta
Field metaField = configuration.getMetaField(object.getClass());
if (metaField != null) {
JsonNode meta = attributesNode.remove(metaField.getName());
if (meta != null && shouldSerializeMeta(settings)) {
dataNode.set(META, meta);
}
}
// Handle links
String selfHref = null;
JsonNode jsonLinks = getResourceLinks(object, attributesNode, resourceId, settings);
if (jsonLinks != null) {
dataNode.set(LINKS, jsonLinks);
if (jsonLinks.has(SELF)) {
selfHref = jsonLinks.get(SELF).get(HREF).asText();
}
}
// Handle resource identifier
dataNode.put(TYPE, configuration.getTypeName(object.getClass()));
if (resourceId != null) {
dataNode.put(ID, resourceId);
// Cache the object for recursion breaking purposes
resourceCache.cache(resourceId.concat(configuration.getTypeName(object.getClass())), null);
}
dataNode.set(ATTRIBUTES, attributesNode);
// Handle relationships (remove from base type and add as relationships)
List relationshipFields = configuration.getRelationshipFields(object.getClass());
if (relationshipFields != null) {
ObjectNode relationshipsNode = objectMapper.createObjectNode();
for (Field relationshipField : relationshipFields) {
Object relationshipObject = relationshipField.get(object);
if (relationshipObject != null) {
attributesNode.remove(relationshipField.getName());
Relationship relationship = configuration.getFieldRelationship(relationshipField);
// In case serialisation is disabled for a given relationship, skip it
if (!relationship.serialise()) {
continue;
}
String relationshipName = relationship.value();
ObjectNode relationshipDataNode = objectMapper.createObjectNode();
relationshipsNode.set(relationshipName, relationshipDataNode);
// Serialize relationship meta
JsonNode relationshipMeta = getRelationshipMeta(object, relationshipName, settings);
if (relationshipMeta != null) {
relationshipDataNode.set(META, relationshipMeta);
attributesNode.remove(configuration
.getRelationshipMetaField(object.getClass(), relationshipName).getName());
}
// Serialize relationship links
JsonNode relationshipLinks = getRelationshipLinks(object, relationship, selfHref, settings);
if (relationshipLinks != null) {
relationshipDataNode.set(LINKS, relationshipLinks);
// Remove link object from serialized JSON
Field refField = configuration
.getRelationshipLinksField(object.getClass(), relationshipName);
if (refField != null) {
attributesNode.remove(refField.getName());
}
}
if (relationshipObject instanceof Collection) {
ArrayNode dataArrayNode = objectMapper.createArrayNode();
for (Object element : (Collection>) relationshipObject) {
String relationshipType = configuration.getTypeName(element.getClass());
String idValue = getIdValue(element);
ObjectNode identifierNode = objectMapper.createObjectNode();
identifierNode.put(TYPE, relationshipType);
identifierNode.put(ID, idValue);
dataArrayNode.add(identifierNode);
// Handle included data
if (shouldSerializeRelationship(relationshipName, settings) && idValue != null) {
String identifier = idValue.concat(relationshipType);
if (!includedContainer.containsKey(identifier) && !resourceCache.contains(identifier)) {
includedContainer.put(identifier,
getDataNode(element, includedContainer, settings));
}
}
}
relationshipDataNode.set(DATA, dataArrayNode);
} else {
String relationshipType = configuration.getTypeName(relationshipObject.getClass());
String idValue = getIdValue(relationshipObject);
ObjectNode identifierNode = objectMapper.createObjectNode();
identifierNode.put(TYPE, relationshipType);
identifierNode.put(ID, idValue);
relationshipDataNode.set(DATA, identifierNode);
if (shouldSerializeRelationship(relationshipName, settings) && idValue != null) {
String identifier = idValue.concat(relationshipType);
if (!includedContainer.containsKey(identifier)) {
includedContainer.put(identifier,
getDataNode(relationshipObject, includedContainer, settings));
}
}
}
}
}
if (relationshipsNode.size() > 0) {
dataNode.set(RELATIONSHIPS, relationshipsNode);
}
}
return dataNode;
}
/**
* Converts input object to byte array.
*
* @param objects List of input objects
* @return raw bytes
* @throws JsonProcessingException
* @throws IllegalAccessException
* @deprecated use writeDocumentCollection instead
*/
@Deprecated
public byte[] writeObjectCollection(Iterable objects) throws JsonProcessingException, IllegalAccessException {
try {
return writeDocumentCollection(new JSONAPIDocument<>(objects));
} catch (DocumentSerializationException e) {
if (e.getCause() instanceof JsonProcessingException) {
throw (JsonProcessingException) e.getCause();
} else if (e.getCause() instanceof IllegalAccessException) {
throw (IllegalAccessException) e.getCause();
}
throw new RuntimeException(e.getCause());
}
}
/**
* Checks if provided type is registered with this converter instance.
* @param type class to check
* @return returns true
if type is registered, else false
*/
public boolean isRegisteredType(Class> type) {
return configuration.isRegisteredType(type);
}
/**
* Returns relationship resolver for given type. In case no specific type resolver is registered, global resolver
* is returned.
* @param type relationship object type
* @return relationship resolver or null
*/
private RelationshipResolver getResolver(Class> type) {
RelationshipResolver resolver = typedResolvers.get(type);
return resolver != null ? resolver : globalResolver;
}
private static class Resource {
private String identifier;
private Object object;
public Resource(String identifier, Object resource) {
this.identifier = identifier;
this.object = resource;
}
public String getIdentifier() {
return identifier;
}
public Object getObject() {
return object;
}
}
/**
* Deserializes a JSON-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;
}
/**
* 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;
}
private ObjectNode addIncludedSection(ObjectNode rootNode, Map includedDataMap) {
if (!includedDataMap.isEmpty()) {
ArrayNode includedArray = objectMapper.createArrayNode();
includedArray.addAll(includedDataMap.values());
rootNode.set(INCLUDED, includedArray);
}
return rootNode;
}
/**
* Resolves actual type to be used for resource deserialization.
*
* If user provides class with type annotation that is equal to the type value in response data, same class
* will be used. If provided class is super type of actual class that is resolved using response type value,
* subclass will be returned. This allows for deserializing responses in use cases where one of many subtypes
* can be returned by the server and user is not sure which one will it be.
*
* @param object JSON object containing type value
* @param userType provided user type
* @return {@link Class}
*/
private Class> getActualType(JsonNode object, Class> userType) {
String type = object.get(TYPE).asText();
String definedTypeName = configuration.getTypeName(userType);
if (definedTypeName != null && definedTypeName.equals(type)) {
return userType;
} else {
Class> actualType = configuration.getTypeClass(type);
if (actualType != null && userType.isAssignableFrom(actualType)) {
return actualType;
}
}
throw new RuntimeException("No class was registered for type '" + type + "'.");
}
private Collection> createCollectionInstance(Class> type)
throws InstantiationException, IllegalAccessException {
if (!type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
return (Collection>) type.newInstance();
}
if (List.class.equals(type) || Collection.class.equals(type)) {
return new ArrayList<>();
}
if (Set.class.equals(type)) {
return new HashSet<>();
}
throw new RuntimeException("Unable to create appropriate instance for type: " + type.getSimpleName());
}
private JsonNode getRelationshipMeta(Object source, String relationshipName, SerializationSettings settings)
throws IllegalAccessException {
if (shouldSerializeMeta(settings)) {
Field relationshipMetaField = configuration
.getRelationshipMetaField(source.getClass(), relationshipName);
if (relationshipMetaField != null && relationshipMetaField.get(source) != null) {
return objectMapper.valueToTree(relationshipMetaField.get(source));
}
}
return null;
}
private JsonNode getResourceLinks(Object resource, ObjectNode serializedResource, String resourceId,
SerializationSettings settings) throws IllegalAccessException {
Type type = configuration.getType(resource.getClass());
// Check if there are user-provided links
Links links = null;
Field linksField = configuration.getLinksField(resource.getClass());
if (linksField != null) {
links = (Links) linksField.get(resource);
// Remove links from attributes object
//TODO: this state change needs to be removed from here
if (links != null) {
serializedResource.remove(linksField.getName());
}
}
// If enabled, handle links
if (shouldSerializeLinks(settings)) {
Map linkMap = new HashMap<>();
if (links != null) {
linkMap.putAll(links.getLinks());
}
// If link path is defined in type and id is not null and user did not explicitly set link value, create it
if (!type.path().trim().isEmpty() && !linkMap.containsKey(SELF) && resourceId != null) {
linkMap.put(SELF, new Link(createURL(baseURL, type.path().replace("{id}", resourceId))));
}
// If there is at least one link generated, serialize and return
if (!linkMap.isEmpty()) {
return objectMapper.valueToTree(new Links(linkMap)).get(LINKS);
}
}
return null;
}
private JsonNode getRelationshipLinks(Object source, Relationship relationship, String ownerLink,
SerializationSettings settings) throws IllegalAccessException {
if (shouldSerializeLinks(settings)) {
Links links = null;
Field relationshipLinksField = configuration
.getRelationshipLinksField(source.getClass(), relationship.value());
if (relationshipLinksField != null) {
links = (Links) relationshipLinksField.get(source);
}
Map linkMap = new HashMap<>();
if (links != null) {
linkMap.putAll(links.getLinks());
}
if (!relationship.path().trim().isEmpty() && !linkMap.containsKey(SELF)) {
linkMap.put(SELF, new Link(createURL(ownerLink, relationship.path())));
}
if (!relationship.relatedPath().trim().isEmpty() && !linkMap.containsKey(RELATED)) {
linkMap.put(RELATED, new Link(createURL(ownerLink, relationship.relatedPath())));
}
if (!linkMap.isEmpty()) {
return objectMapper.valueToTree(new Links(linkMap)).get(LINKS);
}
}
return null;
}
private String createURL(String base, String path) {
String result = base;
if (!result.endsWith("/")) {
result = result.concat("/");
}
if (path.startsWith("/")) {
result = result.concat(path.substring(1));
} else {
result = result.concat(path);
}
return result;
}
private boolean shouldSerializeRelationship(String relationshipName, SerializationSettings settings) {
if (settings != null) {
if (settings.isRelationshipIncluded(relationshipName) && !settings.isRelationshipExcluded(relationshipName)) {
return true;
}
if (settings.isRelationshipExcluded(relationshipName)) {
return false;
}
}
return serializationFeatures.contains(SerializationFeature.INCLUDE_RELATIONSHIP_ATTRIBUTES);
}
private boolean shouldSerializeLinks(SerializationSettings settings) {
if (settings != null && settings.serializeLinks() != null) {
return settings.serializeLinks();
}
return serializationFeatures.contains(SerializationFeature.INCLUDE_LINKS);
}
private boolean shouldSerializeMeta(SerializationSettings settings) {
if (settings != null && settings.serializeMeta() != null) {
return settings.serializeMeta();
}
return serializationFeatures.contains(SerializationFeature.INCLUDE_META);
}
/**
* Registers new type to be used with this converter instance.
* @param type {@link Class} type to register
* @return true
if type was registed, else false
(in case type was registered already or
* type is not eligible for registering ie. missing required annotations)
*/
public boolean registerType(Class> type) {
if (!configuration.isRegisteredType(type) && ConverterConfiguration.isEligibleType(type)) {
return configuration.registerType(type);
}
return false;
}
/**
* Adds (enables) new deserialization option.
* @param option {@link DeserializationFeature} option
*/
public void enableDeserializationOption(DeserializationFeature option) {
this.deserializationFeatures.add(option);
}
/**
* Removes (disables) existing deserialization option.
* @param option {@link DeserializationFeature} feature to disable
*/
public void disableDeserializationOption(DeserializationFeature option) {
this.deserializationFeatures.remove(option);
}
/**
* Adds (enables) new serialization option.
* @param option {@link SerializationFeature} option
*/
public void enableSerializationOption(SerializationFeature option) {
this.serializationFeatures.add(option);
}
/**
* Removes (disables) existing serialization option.
* @param option {@link SerializationFeature} feature to disable
*/
public void disableSerializationOption(SerializationFeature option) {
this.serializationFeatures.remove(option);
}
}