From b57fa851730a585ba8db9f15ca601111065c9842 Mon Sep 17 00:00:00 2001 From: dulleh Date: Sat, 29 Jul 2017 01:28:17 +0100 Subject: [PATCH 1/6] Add @PolymorphRelationships With exceptions to promote use over incorrectly using multiple @Relationship attributes. Tests included and passing. --- .../jsonapi/ConverterConfiguration.java | 141 ++++++++++++++++-- .../jasminb/jsonapi/ResourceConverter.java | 103 +++++++++---- .../annotations/PolymorphRelationship.java | 33 ++++ ...peatedPolymorphRelationshipsException.java | 46 ++++++ .../RepeatedRelationshipsException.java | 33 ++++ .../jsonapi/ResourceConverterTest.java | 78 ++++++++-- .../jsonapi/models/PolymorphParent.java | 31 ++++ .../RepeatedPolymorphRelationships.java | 34 +++++ .../jsonapi/models/RepeatedRelationships.java | 34 +++++ .../polymorph-relationship-author.json | 27 ++++ .../polymorph-relationship-other.json | 27 ++++ .../polymorph-relationship-user.json | 27 ++++ 12 files changed, 564 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/github/jasminb/jsonapi/annotations/PolymorphRelationship.java create mode 100644 src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java create mode 100644 src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedRelationshipsException.java create mode 100644 src/test/java/com/github/jasminb/jsonapi/models/PolymorphParent.java create mode 100644 src/test/java/com/github/jasminb/jsonapi/models/RepeatedPolymorphRelationships.java create mode 100644 src/test/java/com/github/jasminb/jsonapi/models/RepeatedRelationships.java create mode 100644 src/test/resources/polymorph-relationship-author.json create mode 100644 src/test/resources/polymorph-relationship-other.json create mode 100644 src/test/resources/polymorph-relationship-user.json diff --git a/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java b/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java index 6c51df5..4783980 100644 --- a/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java +++ b/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java @@ -1,13 +1,12 @@ package com.github.jasminb.jsonapi; -import com.github.jasminb.jsonapi.annotations.Id; -import com.github.jasminb.jsonapi.annotations.Meta; -import com.github.jasminb.jsonapi.annotations.Relationship; -import com.github.jasminb.jsonapi.annotations.RelationshipLinks; -import com.github.jasminb.jsonapi.annotations.RelationshipMeta; -import com.github.jasminb.jsonapi.annotations.Type; +import com.github.jasminb.jsonapi.annotations.*; +import com.github.jasminb.jsonapi.exceptions.RepeatedPolymorphRelationshipsException; +import com.github.jasminb.jsonapi.exceptions.RepeatedRelationshipsException; +import com.github.jasminb.jsonapi.exceptions.UnregisteredTypeException; import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -37,7 +36,12 @@ public class ConverterConfiguration { private final Map, Class> metaTypeMap = new HashMap<>(); private final Map, Field> metaFieldMap = new HashMap<>(); private final Map, Field> linkFieldMap = new HashMap<>(); - + private final Map, List> polymorphRelationshipMap = new HashMap<>(); + private final Map, Map>>> polymorphRelationshipTypeMap = new HashMap<>(); + private final Map, Map>> polymorphRelationshipFieldMap = new HashMap<>(); + private final Map fieldPolymorphRelationshipMap = new HashMap<>(); + private final Map, Map, Field>>> targetTypePolymorphRelationshipMap = new HashMap<>(); + // Relationship links lookups private final Map, Map> relationshipLinksFieldMap = new HashMap<>(); @@ -58,19 +62,38 @@ private void processClass(Class clazz) { typeAnnotations.put(clazz, annotation); relationshipTypeMap.put(clazz, new HashMap>()); relationshipFieldMap.put(clazz, new HashMap()); + polymorphRelationshipTypeMap.put(clazz, new HashMap>>()); + polymorphRelationshipFieldMap.put(clazz, new HashMap>()); relationshipMetaFieldMap.put(clazz, new HashMap()); relationshipMetaTypeMap.put(clazz, new HashMap>()); relationshipLinksFieldMap.put(clazz, new HashMap()); + targetTypePolymorphRelationshipMap.put(clazz, new HashMap, Field>>()); // collecting Relationship fields List relationshipFields = ReflectionUtils.getAnnotatedFields(clazz, Relationship.class, true); + // collecting PolymorphRelationship fields + List polymorphRelationshipFields = ReflectionUtils.getAnnotatedFields(clazz, PolymorphRelationship.class, true); + // list of all relationship fields + List allRelationshipFields = new ArrayList<>(relationshipFields.size() + polymorphRelationshipFields.size()); + allRelationshipFields.addAll(relationshipFields); + allRelationshipFields.addAll(polymorphRelationshipFields); + + //handle relationship actions that are applicable to either type of relationship + for (Field relationshipField : allRelationshipFields) { + Class targetType = ReflectionUtils.getFieldType(relationshipField); + registerType(targetType); + } + // handle relationships for (Field relationshipField : relationshipFields) { relationshipField.setAccessible(true); Relationship relationship = relationshipField.getAnnotation(Relationship.class); Class targetType = ReflectionUtils.getFieldType(relationshipField); relationshipTypeMap.get(clazz).put(relationship.value(), targetType); + if (relationshipFieldMap.get(clazz).get(relationship.value()) != null) { + throw new RepeatedRelationshipsException(relationship.value()); + } relationshipFieldMap.get(clazz).put(relationship.value(), relationshipField); fieldRelationshipMap.put(relationshipField, relationship); @@ -79,12 +102,55 @@ private void processClass(Class clazz) { relationshipField.getName() + " with 'resolve = true' must have a relType attribute " + "set." ); } + } + relationshipMap.put(clazz, relationshipFields); + + //handle polymorphic relationships + for (Field polymorphRelationshipField : polymorphRelationshipFields) { + polymorphRelationshipField.setAccessible(true); + PolymorphRelationship relationship = polymorphRelationshipField.getAnnotation(PolymorphRelationship.class); + Class targetType = ReflectionUtils.getFieldType(polymorphRelationshipField); + + List> listOfTypesForRelationship = polymorphRelationshipTypeMap.get(clazz).get(relationship.value()); + if (listOfTypesForRelationship == null) { + // Start with two because an @PolymorphRelationship would only be used if >= 2 types could be returned for a relationship + listOfTypesForRelationship = new ArrayList(2); + } + listOfTypesForRelationship.add(targetType); + polymorphRelationshipTypeMap.get(clazz).put(relationship.value(), listOfTypesForRelationship); - registerType(targetType); + List listOfFieldsForRelationship = polymorphRelationshipFieldMap.get(clazz).get(relationship.value()); + if (listOfFieldsForRelationship == null) { + // Start with two because an @PolymorphRelationship would only be used if >= 2 fields could be returned for a relationship + listOfFieldsForRelationship = new ArrayList(2); + } + listOfFieldsForRelationship.add(polymorphRelationshipField); + polymorphRelationshipFieldMap.get(clazz).put(relationship.value(), listOfFieldsForRelationship); + + fieldPolymorphRelationshipMap.put(polymorphRelationshipField, relationship); + + if (relationship.resolve() && relationship.relType() == null) { + throw new IllegalArgumentException("@PolymorphRelationship on " + clazz.getName() + "#" + + polymorphRelationshipField.getName() + " with 'resolve = true' must have a relType attribute " + + "set." ); + } + + Map, Field> targetTypesToFieldsMap = targetTypePolymorphRelationshipMap.get(clazz).get(relationship.value()); + if (targetTypesToFieldsMap == null) { + // initialisation + targetTypesToFieldsMap = new HashMap(); + targetTypePolymorphRelationshipMap.get(clazz).put(relationship.value(), targetTypesToFieldsMap); + } + Field targetField = targetTypesToFieldsMap.get(targetType); + if (targetField == null) { + targetTypePolymorphRelationshipMap.get(clazz).get(relationship.value()).put(targetType, polymorphRelationshipField); + } else { + throw new RepeatedPolymorphRelationshipsException(relationship.value(), ReflectionUtils.getTypeName(targetType)); + } } - relationshipMap.put(clazz, relationshipFields); - + polymorphRelationshipMap.put(clazz, polymorphRelationshipFields); + // collecting RelationshipMeta fields List relMetaFields = ReflectionUtils.getAnnotatedFields(clazz, RelationshipMeta.class, true); @@ -268,6 +334,61 @@ public List getRelationshipFields(Class clazz) { return relationshipMap.get(clazz); } + /** + * Returns all polymorphic relationship fields for a specific relationship name. + * @param clazz {@link Class} class holding the polymorphic relationship + * @param fieldName {@link String} name of the relationship + * @return {@link Field} field + */ + public List getPolymorphRelationshipFields(Class clazz, String fieldName) { + return polymorphRelationshipFieldMap.get(clazz).get(fieldName); + } + + /** + * Returns the types a polymorphic relationship can have. + * @param clazz {@link Class} owning the field with {@link PolymorphRelationship} annotation + * @param fieldName {@link String} name of the relationship + * @return {@link Class} field type + */ + public List> getPolymorphRelationshipType(Class clazz, String fieldName) { + return polymorphRelationshipTypeMap.get(clazz).get(fieldName); + } + + /** + * Resolves {@link PolymorphRelationship} instance for given field. + * + *

+ * This works for fields that were found to be annotated with {@link PolymorphRelationship} annotation. + *

+ * @param field {@link Field} to get the polymorphic relationship for + * @return {@link PolymorphRelationship} anotation or null + */ + public PolymorphRelationship getFieldPolymorphRelationship(Field field) { + return fieldPolymorphRelationshipMap.get(field); + } + + /** + * Returns list of all fields annotated with {@link PolymorphRelationship} annotation for given class. + * @param clazz {@link Class} to get relationship fields for + * @return list of relationship fields + */ + public List getPolymorphRelationshipFields(Class clazz) { + return polymorphRelationshipMap.get(clazz); + } + + /** + * Returns the field for a specific polymorphic relationship. + * @param clazz {@link Class} to get relationship fields for + * @param fieldName the name of the relationship + * @param targetType the type the object should be parsed to + * @return list of relationship fields + */ + public Field getPolymorphRelationshipField(Class clazz, String fieldName, Class targetType) { + Field field = targetTypePolymorphRelationshipMap.get(clazz).get(fieldName).get(targetType); + if (field != null) return field; + throw new UnregisteredTypeException("No type specified for polymorphic relationship \"" + fieldName + "\"."); + } + /** * Checks if provided class was registered with this configuration instance. * @param clazz {@link Class} to check diff --git a/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java b/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java index fac80f9..0a27285 100644 --- a/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java +++ b/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java @@ -10,6 +10,7 @@ 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.PolymorphRelationship; import com.github.jasminb.jsonapi.annotations.Relationship; import com.github.jasminb.jsonapi.annotations.Type; import com.github.jasminb.jsonapi.exceptions.DocumentSerializationException; @@ -21,14 +22,7 @@ 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 java.util.*; import static com.github.jasminb.jsonapi.JSONAPISpecConstants.*; @@ -446,20 +440,43 @@ private void handleRelationships(JsonNode source, Object object) JsonNode relationship = relationships.get(field); Field relationshipField = configuration.getRelationshipField(object.getClass(), field); + // target type + Class type = null; + // if there is no @Relationship defined for this particular relationship name, check for @PolymorphRelationships + if (relationshipField == null) { + // Get list of possible target types + List> possibleRelationshipTypes = configuration.getPolymorphRelationshipType(object.getClass(), field); + + // if there are no fields to map the json object to (field is not set as name of any relationships in the class) cannot parse + if (possibleRelationshipTypes == null || possibleRelationshipTypes.isEmpty()) continue; + // if there aren't data and type attributes set we can't parse + if (!relationship.has(DATA) || !relationship.get(DATA).has(TYPE)) continue; + // check if the field is one we can convert to + String jsonType = relationship.get(DATA).get(TYPE).asText(); + for (Class clash : possibleRelationshipTypes) { + // if the type is one we have defined + if (Objects.equals(ReflectionUtils.getTypeName(clash), jsonType)) { + // set the targetType + type = clash; + // get the correct field + relationshipField = configuration.getPolymorphRelationshipField(object.getClass(), field, type); + } + } + } if (relationshipField != null) { - // Get target type - Class type = configuration.getRelationshipType(object.getClass(), field); - + // Get target type if it wasn't already set a polymorph relationship + if (type == null) { + 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))); @@ -476,7 +493,14 @@ private void handleRelationships(JsonNode source, Object object) } // Get resolve flag - boolean resolveRelationship = configuration.getFieldRelationship(relationshipField).resolve(); + //TODO: add to fieldRelationship regardless of polymorph or not (getFieldRelationship needs to be used for either) + Relationship classRelationship = configuration.getFieldRelationship(relationshipField); + boolean resolveRelationship; + if (classRelationship == null) { + resolveRelationship = configuration.getFieldPolymorphRelationship(relationshipField).resolve(); + } else { + resolveRelationship = configuration.getFieldRelationship(relationshipField).resolve(); + } RelationshipResolver resolver = getResolver(type); // Use resolver if possible @@ -803,10 +827,16 @@ private ObjectNode getDataNode(Object object, Map includedCo } dataNode.set(ATTRIBUTES, attributesNode); - // Handle relationships (remove from base type and add as relationships) + // Handle Relationships (remove from base type and add as relationships) List relationshipFields = configuration.getRelationshipFields(object.getClass()); - - if (relationshipFields != null) { + // Get the PolymorphRelationship fields + List polymorphRelationshipFields = configuration.getPolymorphRelationshipFields(object.getClass()); + //Combine the two lists + List allRelationshipFields = new ArrayList<>(relationshipFields.size() + polymorphRelationshipFields.size()); + allRelationshipFields.addAll(relationshipFields); + allRelationshipFields.addAll(polymorphRelationshipFields); + + if (!allRelationshipFields.isEmpty()) { ObjectNode relationshipsNode = objectMapper.createObjectNode(); for (Field relationshipField : relationshipFields) { @@ -816,14 +846,30 @@ private ObjectNode getDataNode(Object object, Map includedCo attributesNode.remove(namingStrategy.nameForField(null, null, relationshipField.getName())); Relationship relationship = configuration.getFieldRelationship(relationshipField); + PolymorphRelationship polymorphRelationship = configuration.getFieldPolymorphRelationship(relationshipField); // In case serialisation is disabled for a given relationship, skip it - if (!relationship.serialise()) { + if ((relationship != null && !relationship.serialise()) + || (polymorphRelationship != null && !polymorphRelationship.serialise())) { continue; } - String relationshipName = relationship.value(); - + String relationshipName = null; + String relationshipPath = null; + String relationshipRelatedPath = null; + if (relationship != null) { + relationshipName = relationship.value(); + relationshipPath = relationship.path(); + relationshipRelatedPath = relationship.relatedPath(); + } else if (polymorphRelationship != null) { + relationshipName = polymorphRelationship.value(); + relationshipPath = polymorphRelationship.path(); + relationshipRelatedPath = polymorphRelationship.relatedPath(); + } + // set to null to explicitly declare that the rest of the method does not need to concern the two relationship types + relationship = null; + polymorphRelationship = null; + ObjectNode relationshipDataNode = objectMapper.createObjectNode(); relationshipsNode.set(relationshipName, relationshipDataNode); @@ -836,7 +882,7 @@ private ObjectNode getDataNode(Object object, Map includedCo } // Serialize relationship links - JsonNode relationshipLinks = getRelationshipLinks(object, relationship, selfHref, settings); + JsonNode relationshipLinks = getRelationshipLinks(object, relationshipName, relationshipPath, relationshipRelatedPath, selfHref, settings); if (relationshipLinks != null) { relationshipDataNode.set(LINKS, relationshipLinks); @@ -1134,13 +1180,14 @@ private JsonNode getResourceLinks(Object resource, ObjectNode serializedResource return null; } - private JsonNode getRelationshipLinks(Object source, Relationship relationship, String ownerLink, - SerializationSettings settings) throws IllegalAccessException { + private JsonNode getRelationshipLinks(Object source, String relationshipValue, + String relationshipPath, String relationshipRelatedPath, + String ownerLink, SerializationSettings settings) throws IllegalAccessException { if (shouldSerializeLinks(settings)) { Links links = null; Field relationshipLinksField = configuration - .getRelationshipLinksField(source.getClass(), relationship.value()); + .getRelationshipLinksField(source.getClass(), relationshipValue); if (relationshipLinksField != null) { links = (Links) relationshipLinksField.get(source); @@ -1152,12 +1199,12 @@ private JsonNode getRelationshipLinks(Object source, Relationship relationship, linkMap.putAll(links.getLinks()); } - if (!relationship.path().trim().isEmpty() && !linkMap.containsKey(SELF)) { - linkMap.put(SELF, new Link(createURL(ownerLink, relationship.path()))); + if (!relationshipPath.trim().isEmpty() && !linkMap.containsKey(SELF)) { + linkMap.put(SELF, new Link(createURL(ownerLink, relationshipPath))); } - if (!relationship.relatedPath().trim().isEmpty() && !linkMap.containsKey(RELATED)) { - linkMap.put(RELATED, new Link(createURL(ownerLink, relationship.relatedPath()))); + if (!relationshipRelatedPath.trim().isEmpty() && !linkMap.containsKey(RELATED)) { + linkMap.put(RELATED, new Link(createURL(ownerLink, relationshipRelatedPath))); } if (!linkMap.isEmpty()) { diff --git a/src/main/java/com/github/jasminb/jsonapi/annotations/PolymorphRelationship.java b/src/main/java/com/github/jasminb/jsonapi/annotations/PolymorphRelationship.java new file mode 100644 index 0000000..c0cce84 --- /dev/null +++ b/src/main/java/com/github/jasminb/jsonapi/annotations/PolymorphRelationship.java @@ -0,0 +1,33 @@ +package com.github.jasminb.jsonapi.annotations; + +import com.github.jasminb.jsonapi.RelType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to configure relationship field in JSON API resources. + * + * @author jbegic + */ + +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PolymorphRelationship { + String value(); + boolean resolve() default false; + boolean serialise() default true; + RelType relType() default RelType.SELF; + + /** + * Resource path, used to generate self link. + */ + String path() default ""; + + /** + * Resource path, used to generate related link. + */ + String relatedPath() default ""; +} diff --git a/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java b/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java new file mode 100644 index 0000000..af04f43 --- /dev/null +++ b/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java @@ -0,0 +1,46 @@ +package com.github.jasminb.jsonapi.exceptions; + +/** + * RepeatedPolymorphRelationshipsException + * Thrown when a @PolymorphRelationship is defined more than once with the same type within a class. + * + * @author dulleh. + */ +public class RepeatedPolymorphRelationshipsException extends RuntimeException { + + private final String relationshipName; + private final String targetType; + + /** + * Constructor. + * + * @param relationshipName The relationship name of the relationship + * @param targetType The type for which the relationship is repeated. + */ + public RepeatedPolymorphRelationshipsException(String relationshipName, String targetType) { + super("@PolymorphRelationship(" + relationshipName + ") set on multiple fields of the type " + targetType + + ". Fields annotated with @PolymorphRelationship must have unique types defined."); + this.relationshipName = relationshipName; + this.targetType = targetType; + System.out.println("relationshipName: " + relationshipName); + System.out.println("targetType: " + targetType); + } + + /** + * Returns the repeated relationship name which caused the exception. + * + * @return + */ + public String getType() { + return relationshipName; + } + + /** + * Returns the repeated type which caused the exception. + * + * @return + */ + public String getTargetType() { + return targetType; + } +} diff --git a/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedRelationshipsException.java b/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedRelationshipsException.java new file mode 100644 index 0000000..9608817 --- /dev/null +++ b/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedRelationshipsException.java @@ -0,0 +1,33 @@ +package com.github.jasminb.jsonapi.exceptions; + +/** + * RepeatedRelationshipsException + * Thrown when a @Relationship is defined more than once with the same name within a class. + * + * @author dulleh. + */ +public class RepeatedRelationshipsException extends RuntimeException { + + private final String relationshipName; + + /** + * Constructor. + * + * @param relationshipName The relationship name that is registered more than once. + */ + public RepeatedRelationshipsException(String relationshipName) { + super("@Relationship(" + relationshipName + ") set on multiple fields. " + + "If the json returned for this relationship can be of multiple types (polymorphic), " + + "please use @PolymorphicRelationship."); + this.relationshipName = relationshipName; + } + + /** + * Returns the repeated relationship name which caused the exception. + * + * @return + */ + public String getType() { + return relationshipName; + } +} diff --git a/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java b/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java index fb49fa6..229fcf4 100644 --- a/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java +++ b/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java @@ -6,18 +6,10 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.github.jasminb.jsonapi.exceptions.DocumentSerializationException; import com.github.jasminb.jsonapi.exceptions.InvalidJsonApiResourceException; +import com.github.jasminb.jsonapi.exceptions.RepeatedPolymorphRelationshipsException; +import com.github.jasminb.jsonapi.exceptions.RepeatedRelationshipsException; import com.github.jasminb.jsonapi.exceptions.UnregisteredTypeException; -import com.github.jasminb.jsonapi.models.Article; -import com.github.jasminb.jsonapi.models.Author; -import com.github.jasminb.jsonapi.models.Comment; -import com.github.jasminb.jsonapi.models.IntegerIdResource; -import com.github.jasminb.jsonapi.models.LongIdResource; -import com.github.jasminb.jsonapi.models.NoDefaultConstructorClass; -import com.github.jasminb.jsonapi.models.NoIdAnnotationModel; -import com.github.jasminb.jsonapi.models.RecursingNode; -import com.github.jasminb.jsonapi.models.SimpleMeta; -import com.github.jasminb.jsonapi.models.Status; -import com.github.jasminb.jsonapi.models.User; +import com.github.jasminb.jsonapi.models.*; import com.github.jasminb.jsonapi.models.inheritance.BaseModel; import com.github.jasminb.jsonapi.models.inheritance.City; import com.github.jasminb.jsonapi.models.inheritance.Engineer; @@ -55,7 +47,69 @@ public void setup() { converter = new ResourceConverter("https://api.example.com", Status.class, User.class, Author.class, Article.class, Comment.class, Engineer.class, EngineeringField.class, City.class, IntegerIdResource.class, LongIdResource.class, - NoDefaultConstructorClass.class); + NoDefaultConstructorClass.class, PolymorphParent.class); + } + + @Test + public void testReadPolymorphicRelationshipUser() throws IOException { + InputStream parentStream = IOUtils.getResource("polymorph-relationship-user.json"); + + JSONAPIDocument parentDoc = converter.readDocument(parentStream, PolymorphParent.class); + PolymorphParent parent = parentDoc.get(); + + Assert.assertNotNull(parentDoc); + Assert.assertNotNull(parent); + if (parent.user != null) { + Assert.assertEquals("1", parent.user.id); + Assert.assertEquals("sam_i_am", parent.user.getName()); + } else { + Assert.fail(); + } + } + + @Test + public void testReadPolymorphicRelationshipAuthor() throws IOException { + InputStream parentStream = IOUtils.getResource("polymorph-relationship-author.json"); + + JSONAPIDocument parentDoc = converter.readDocument(parentStream, PolymorphParent.class); + PolymorphParent parent = parentDoc.get(); + + Assert.assertNotNull(parentDoc); + Assert.assertNotNull(parent); + if (parent.author != null) { + Assert.assertEquals("1", parent.author.getId()); + Assert.assertEquals("sam_i_am", parent.author.getFirstName()); + } else { + Assert.fail(); + } + } + + @Test + public void testReadPolymorphicRelationshipOther() throws IOException { + InputStream parentStream = IOUtils.getResource("polymorph-relationship-other.json"); + + converter.enableDeserializationOption(DeserializationFeature.ALLOW_UNKNOWN_INCLUSIONS); + JSONAPIDocument parentDoc = converter.readDocument(parentStream, PolymorphParent.class); + PolymorphParent parent = parentDoc.get(); + + Assert.assertNotNull(parentDoc); + Assert.assertNotNull(parent); + Assert.assertNull(parent.user); + Assert.assertNull(parent.author); + } + + @Test(expected = RepeatedRelationshipsException.class) + public void testRepeatedRelationshipsException() throws IOException { + ResourceConverter converter = new ResourceConverter("https://api.example.com", RepeatedRelationships.class); + InputStream parentStream = IOUtils.getResource("polymorph-relationship-user.json"); + converter.readDocument(parentStream, RepeatedRelationships.class); + } + + @Test(expected = RepeatedPolymorphRelationshipsException.class) + public void testRepeatedPoylmorphRelationshipsException() throws IOException { + ResourceConverter converter = new ResourceConverter("https://api.example.com", RepeatedPolymorphRelationships.class); + InputStream parentStream = IOUtils.getResource("polymorph-relationship-user.json"); + converter.readDocument(parentStream, RepeatedPolymorphRelationships.class); } @Test diff --git a/src/test/java/com/github/jasminb/jsonapi/models/PolymorphParent.java b/src/test/java/com/github/jasminb/jsonapi/models/PolymorphParent.java new file mode 100644 index 0000000..e415785 --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/models/PolymorphParent.java @@ -0,0 +1,31 @@ +package com.github.jasminb.jsonapi.models; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.github.jasminb.jsonapi.annotations.Id; +import com.github.jasminb.jsonapi.annotations.PolymorphRelationship; +import com.github.jasminb.jsonapi.annotations.Relationship; +import com.github.jasminb.jsonapi.annotations.Type; + +@Type("polymorph-parent") +@JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class, property = "id") +@JsonIgnoreProperties(ignoreUnknown = true) +public class PolymorphParent { + @Id + private String id; + + @PolymorphRelationship("arbitraryRelationship") + public User user; + + @PolymorphRelationship("arbitraryRelationship") + public Author author; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/src/test/java/com/github/jasminb/jsonapi/models/RepeatedPolymorphRelationships.java b/src/test/java/com/github/jasminb/jsonapi/models/RepeatedPolymorphRelationships.java new file mode 100644 index 0000000..d592f63 --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/models/RepeatedPolymorphRelationships.java @@ -0,0 +1,34 @@ +package com.github.jasminb.jsonapi.models; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.github.jasminb.jsonapi.annotations.Id; +import com.github.jasminb.jsonapi.annotations.PolymorphRelationship; +import com.github.jasminb.jsonapi.annotations.Relationship; +import com.github.jasminb.jsonapi.annotations.Type; + +@Type("polymorph-parent") +@JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class, property = "id") +@JsonIgnoreProperties(ignoreUnknown = true) +public class RepeatedPolymorphRelationships { + @Id + private String id; + + /* + * Notice: two @PolymorphRelationship of same type + */ + @PolymorphRelationship("arbitraryRelationship") + public User user; + + @PolymorphRelationship("arbitraryRelationship") + public User secondUser; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/src/test/java/com/github/jasminb/jsonapi/models/RepeatedRelationships.java b/src/test/java/com/github/jasminb/jsonapi/models/RepeatedRelationships.java new file mode 100644 index 0000000..592742e --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/models/RepeatedRelationships.java @@ -0,0 +1,34 @@ +package com.github.jasminb.jsonapi.models; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.github.jasminb.jsonapi.annotations.Id; +import com.github.jasminb.jsonapi.annotations.PolymorphRelationship; +import com.github.jasminb.jsonapi.annotations.Relationship; +import com.github.jasminb.jsonapi.annotations.Type; + +@Type("polymorph-parent") +@JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class, property = "id") +@JsonIgnoreProperties(ignoreUnknown = true) +public class RepeatedRelationships { + @Id + private String id; + + /* + * Notice: two @Relationship (not polymorph) of same name + */ + @Relationship("arbitraryRelationship") + public User user; + + @Relationship("arbitraryRelationship") + public Author author; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/src/test/resources/polymorph-relationship-author.json b/src/test/resources/polymorph-relationship-author.json new file mode 100644 index 0000000..9ba4f4b --- /dev/null +++ b/src/test/resources/polymorph-relationship-author.json @@ -0,0 +1,27 @@ +{ + "data":{ + "id":"someId", + "type":"polymorph-parent", + "relationships":{ + "arbitraryRelationship":{ + "links":{ + "self":"https://api.com/w/e", + "related":"https://api.com/w/e" + }, + "data":{ + "type":"people", + "id":"1" + } + } + } + }, + "included":[ + { + "type": "people", + "id": "1", + "attributes": { + "firstName": "sam_i_am" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/polymorph-relationship-other.json b/src/test/resources/polymorph-relationship-other.json new file mode 100644 index 0000000..c56ad74 --- /dev/null +++ b/src/test/resources/polymorph-relationship-other.json @@ -0,0 +1,27 @@ +{ + "data":{ + "id":"someId", + "type":"polymorph-parent", + "relationships":{ + "arbitraryRelationship":{ + "links":{ + "self":"https://api.com/w/e", + "related":"https://api.com/w/e" + }, + "data":{ + "type":"other", + "id":"1" + } + } + } + }, + "included":[ + { + "type": "other", + "id": "1", + "attributes": { + "alien": "ALIENS!!!" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/polymorph-relationship-user.json b/src/test/resources/polymorph-relationship-user.json new file mode 100644 index 0000000..0d19d4c --- /dev/null +++ b/src/test/resources/polymorph-relationship-user.json @@ -0,0 +1,27 @@ +{ + "data":{ + "id":"someId", + "type":"polymorph-parent", + "relationships":{ + "arbitraryRelationship":{ + "links":{ + "self":"https://api.com/w/e", + "related":"https://api.com/w/e" + }, + "data":{ + "type":"users", + "id":"1" + } + } + } + }, + "included":[ + { + "type": "users", + "id": "1", + "attributes": { + "name": "sam_i_am" + } + } + ] +} \ No newline at end of file From bdf3ddc878a6d1410f8466db0d72abff471417f8 Mon Sep 17 00:00:00 2001 From: dulleh Date: Sat, 29 Jul 2017 14:09:31 +0100 Subject: [PATCH 2/6] Remove print statements --- .../exceptions/RepeatedPolymorphRelationshipsException.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java b/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java index af04f43..001f218 100644 --- a/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java +++ b/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java @@ -22,8 +22,6 @@ public RepeatedPolymorphRelationshipsException(String relationshipName, String t ". Fields annotated with @PolymorphRelationship must have unique types defined."); this.relationshipName = relationshipName; this.targetType = targetType; - System.out.println("relationshipName: " + relationshipName); - System.out.println("targetType: " + targetType); } /** From 12b7370c4abf446e8e597ba2123a386725260b3a Mon Sep 17 00:00:00 2001 From: dulleh Date: Sat, 29 Jul 2017 14:31:04 +0100 Subject: [PATCH 3/6] Added class to Relationship errors More helpful. --- .../jasminb/jsonapi/ConverterConfiguration.java | 4 ++-- ...RepeatedPolymorphRelationshipsException.java | 17 +++++++++++++++-- .../RepeatedRelationshipsException.java | 17 +++++++++++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java b/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java index 4783980..d1e2356 100644 --- a/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java +++ b/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java @@ -92,7 +92,7 @@ private void processClass(Class clazz) { Class targetType = ReflectionUtils.getFieldType(relationshipField); relationshipTypeMap.get(clazz).put(relationship.value(), targetType); if (relationshipFieldMap.get(clazz).get(relationship.value()) != null) { - throw new RepeatedRelationshipsException(relationship.value()); + throw new RepeatedRelationshipsException(relationship.value(), clazz); } relationshipFieldMap.get(clazz).put(relationship.value(), relationshipField); fieldRelationshipMap.put(relationshipField, relationship); @@ -145,7 +145,7 @@ private void processClass(Class clazz) { if (targetField == null) { targetTypePolymorphRelationshipMap.get(clazz).get(relationship.value()).put(targetType, polymorphRelationshipField); } else { - throw new RepeatedPolymorphRelationshipsException(relationship.value(), ReflectionUtils.getTypeName(targetType)); + throw new RepeatedPolymorphRelationshipsException(relationship.value(), ReflectionUtils.getTypeName(targetType), clazz); } } diff --git a/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java b/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java index 001f218..ab3bb8a 100644 --- a/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java +++ b/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedPolymorphRelationshipsException.java @@ -10,18 +10,21 @@ public class RepeatedPolymorphRelationshipsException extends RuntimeException { private final String relationshipName; private final String targetType; + private final Class clazz; /** * Constructor. * * @param relationshipName The relationship name of the relationship * @param targetType The type for which the relationship is repeated. + * @param clazz The class which was being parsed when the error was thrown. */ - public RepeatedPolymorphRelationshipsException(String relationshipName, String targetType) { + public RepeatedPolymorphRelationshipsException(String relationshipName, String targetType, Class clazz) { super("@PolymorphRelationship(" + relationshipName + ") set on multiple fields of the type " + targetType + - ". Fields annotated with @PolymorphRelationship must have unique types defined."); + " in " + clazz + ". Fields annotated with @PolymorphRelationship must have unique types defined."); this.relationshipName = relationshipName; this.targetType = targetType; + this.clazz = clazz; } /** @@ -41,4 +44,14 @@ public String getType() { public String getTargetType() { return targetType; } + + /** + * Returns the class which caused the exception. + * + * @return + */ + public String getClazz() { + return relationshipName; + } + } diff --git a/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedRelationshipsException.java b/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedRelationshipsException.java index 9608817..2339b94 100644 --- a/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedRelationshipsException.java +++ b/src/main/java/com/github/jasminb/jsonapi/exceptions/RepeatedRelationshipsException.java @@ -9,17 +9,20 @@ public class RepeatedRelationshipsException extends RuntimeException { private final String relationshipName; + private final Class clazz; /** * Constructor. * * @param relationshipName The relationship name that is registered more than once. + * @param clazz The class being parsed when the error was thrown. */ - public RepeatedRelationshipsException(String relationshipName) { - super("@Relationship(" + relationshipName + ") set on multiple fields. " + + public RepeatedRelationshipsException(String relationshipName, Class clazz) { + super("@Relationship(" + relationshipName + ") set on multiple fields in " + clazz + ". " + "If the json returned for this relationship can be of multiple types (polymorphic), " + "please use @PolymorphicRelationship."); this.relationshipName = relationshipName; + this.clazz = clazz; } /** @@ -30,4 +33,14 @@ public RepeatedRelationshipsException(String relationshipName) { public String getType() { return relationshipName; } + + /** + * Returns the class which caused the exception. + * + * @return + */ + public String getClazz() { + return relationshipName; + } + } From 8e4c81fd3e893628857b3b0f8f0ea78c50b9b937 Mon Sep 17 00:00:00 2001 From: dulleh Date: Sat, 29 Jul 2017 22:16:21 +0100 Subject: [PATCH 4/6] More polymorph tests Those relating to RelationshipLinks appear to be failing --- .../jsonapi/ResourceConverterTest.java | 114 +++++++++++++++++- .../jsonapi/models/ArticlePolymorph.java | 61 ++++++++++ .../jsonapi/models/StatusPolymorph.java | 112 +++++++++++++++++ src/test/resources/articles-polymorph.json | 71 +++++++++++ src/test/resources/status-polymorph.json | 48 ++++++++ 5 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/github/jasminb/jsonapi/models/ArticlePolymorph.java create mode 100644 src/test/java/com/github/jasminb/jsonapi/models/StatusPolymorph.java create mode 100644 src/test/resources/articles-polymorph.json create mode 100644 src/test/resources/status-polymorph.json diff --git a/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java b/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java index 229fcf4..0a4e6a4 100644 --- a/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java +++ b/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java @@ -46,8 +46,9 @@ public class ResourceConverterTest { public void setup() { converter = new ResourceConverter("https://api.example.com", Status.class, User.class, Author.class, Article.class, Comment.class, Engineer.class, EngineeringField.class, City.class, + PolymorphParent.class, StatusPolymorph.class, ArticlePolymorph.class, IntegerIdResource.class, LongIdResource.class, - NoDefaultConstructorClass.class, PolymorphParent.class); + NoDefaultConstructorClass.class); } @Test @@ -156,6 +157,51 @@ public void testReadWriteObject() throws Exception { } + + @Test + public void testReadWriteObjectWithPolymorph() throws Exception { + StatusPolymorph status = new StatusPolymorph(); + status.setContent("content"); + status.setCommentCount(1); + status.setLikeCount(10); + status.setId("id"); + status.setUser(new User()); + status.getUser().setId("userid"); + status.setRelatedUser(status.getUser()); + + byte [] rawData = converter.writeDocument(new JSONAPIDocument<>(status)); + + Assert.assertNotNull(rawData); + Assert.assertFalse(rawData.length == 0); + + JSONAPIDocument convertedDocument = converter.readDocument(new ByteArrayInputStream(rawData), StatusPolymorph.class); + StatusPolymorph converted = convertedDocument.get(); + // Make sure relationship with disabled serialisation is not present + Assert.assertNull(converted.getRelatedUser()); + + Assert.assertEquals(status.getId(), converted.getId()); + Assert.assertEquals(status.getLikeCount(), converted.getLikeCount()); + Assert.assertEquals(status.getCommentCount(), converted.getCommentCount()); + Assert.assertEquals(status.getContent(), converted.getContent()); + + + Assert.assertNotNull(converted.getUser()); + Assert.assertEquals(status.getUser().getId(), converted.getUser().getId()); + + // Make sure type link is present + Assert.assertNotNull(converted.getLinks()); + Assert.assertEquals("https://api.example.com/statuses/id", + converted.getLinks().getSelf().getHref()); + + // Make sure relationship links are present + Assert.assertNotNull(converted.getUserRelationshipLinks()); + Assert.assertEquals("https://api.example.com/statuses/id/relationships/user", + converted.getUserRelationshipLinks().getSelf().getHref()); + Assert.assertEquals("https://api.example.com/statuses/id/user", + converted.getUserRelationshipLinks().getRelated().getHref()); + + } + @Test public void testReadWithIncludedSection() throws IOException { InputStream apiResponse = IOUtils.getResource("status.json"); @@ -373,6 +419,51 @@ public void testIncludedFullRelationships() throws IOException { Assert.assertEquals("dgeb", commentWithAuthor.getAuthor().getTwitter()); } + + @Test + public void testIncludedFullRelationshipsPolymorph() throws IOException { //TODO: make polymorph + InputStream apiResponse = IOUtils.getResource("articles-polymorph.json"); + + ObjectMapper articlesMapper = new ObjectMapper(); + articlesMapper.setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); + + ResourceConverter articlesConverter = new ResourceConverter(articlesMapper, ArticlePolymorph.class, Author.class, + Comment.class); + + JSONAPIDocument> articlesDocument = articlesConverter.readDocumentCollection(apiResponse, ArticlePolymorph.class); + List articles = articlesDocument.get(); + + Assert.assertNotNull(articles); + Assert.assertEquals(1, articles.size()); + + ArticlePolymorph article = articles.get(0); + + Assert.assertEquals("JSON API paints my bikeshed!", article.getTitle()); + Assert.assertEquals("1", article.getId()); + + Assert.assertNotNull(article.getAuthor()); + + Author author = article.getAuthor(); + + Assert.assertEquals("9", author.getId()); + Assert.assertEquals("Dan", author.getFirstName()); + + Assert.assertNotNull(article.getComments()); + + List comments = article.getComments(); + + Assert.assertEquals(2, comments.size()); + + Comment commentWithAuthor = comments.get(1); + + Assert.assertEquals("12", commentWithAuthor.getId()); + Assert.assertEquals("I like XML better", commentWithAuthor.getBody()); + + Assert.assertNotNull(commentWithAuthor.getAuthor()); + Assert.assertEquals("9", commentWithAuthor.getAuthor().getId()); + Assert.assertEquals("dgeb", commentWithAuthor.getAuthor().getTwitter()); + } + @Test public void testReadWithCollectionInvalidRelationships() throws IOException { InputStream apiResponse = IOUtils.getResource("user-with-invalid-relationships.json"); @@ -723,6 +814,27 @@ public void testWriteRelationshipLinks() throws IOException, DocumentSerializati Assert.assertNotNull(status.getUserRelationshipLinks()); Assert.assertEquals("users/userid", status.getUserRelationshipLinks().getSelf().getHref()); } + + @Test + public void testReadPolymorphRelationshipLinks() throws IOException { + InputStream statusStream = IOUtils.getResource("status-polymorph.json"); + StatusPolymorph status = converter.readDocument(statusStream, StatusPolymorph.class).get(); + + Assert.assertNotNull(status.getUserRelationshipLinks()); + Assert.assertEquals("users/userid", status.getUserRelationshipLinks().getSelf().getHref()); + } + + @Test + public void testWritePolymorphRelationshipLinks() throws IOException, DocumentSerializationException { + InputStream statusStream = IOUtils.getResource("status-polymorph.json"); + JSONAPIDocument statusJSONAPIDocument = converter.readDocument(statusStream, StatusPolymorph.class); + + byte [] serialized = converter.writeDocument(statusJSONAPIDocument); + + StatusPolymorph status = converter.readDocument(serialized, StatusPolymorph.class).get(); + Assert.assertNotNull(status.getUserRelationshipLinks()); + Assert.assertEquals("users/userid", status.getUserRelationshipLinks().getSelf().getHref()); + } @Test public void testReadMetaOnly() { diff --git a/src/test/java/com/github/jasminb/jsonapi/models/ArticlePolymorph.java b/src/test/java/com/github/jasminb/jsonapi/models/ArticlePolymorph.java new file mode 100644 index 0000000..c06cb90 --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/models/ArticlePolymorph.java @@ -0,0 +1,61 @@ +package com.github.jasminb.jsonapi.models; + + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.github.jasminb.jsonapi.RelType; +import com.github.jasminb.jsonapi.annotations.Id; +import com.github.jasminb.jsonapi.annotations.PolymorphRelationship; +import com.github.jasminb.jsonapi.annotations.Relationship; +import com.github.jasminb.jsonapi.annotations.Type; + +import java.util.List; + +@Type("articles-p") +@JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class, property = "id") +public class ArticlePolymorph { + @Id + private String id; + + private String title; + + // Notice: Polymorphic + @PolymorphRelationship(value = "author", resolve = true, relType = RelType.RELATED) + private Author author; + + @Relationship(value = "comments", resolve = true) + 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 Author getAuthor() { + return author; + } + + public void setAuthor(Author 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/models/StatusPolymorph.java b/src/test/java/com/github/jasminb/jsonapi/models/StatusPolymorph.java new file mode 100644 index 0000000..a555a4c --- /dev/null +++ b/src/test/java/com/github/jasminb/jsonapi/models/StatusPolymorph.java @@ -0,0 +1,112 @@ +package com.github.jasminb.jsonapi.models; + +import com.github.jasminb.jsonapi.Links; +import com.github.jasminb.jsonapi.annotations.*; + +@Type(value = "statuses-p", path = "/statuses/{id}") +public class StatusPolymorph { + @Id + private String id; + private String content; + private Integer commentCount; + private Integer likeCount; + + @com.github.jasminb.jsonapi.annotations.Links + private Links links; + + // Notice: @PolymorphRelationship + @PolymorphRelationship(value = "user", resolve = true, path = "/relationships/user", relatedPath = "user") + private User user; + + @RelationshipMeta("user") + private SimpleMeta userRelationshipMeta; + + @RelationshipLinks("user") + private Links userRelationshipLinks; + + @Relationship(value = "related-user", resolve = true, serialise = false) + private User relatedUser; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Integer getCommentCount() { + return commentCount; + } + + public void setCommentCount(Integer commentCount) { + this.commentCount = commentCount; + } + + public Integer getLikeCount() { + return likeCount; + } + + public void setLikeCount(Integer likeCount) { + this.likeCount = likeCount; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public User getRelatedUser() { + return relatedUser; + } + + public void setRelatedUser(User relatedUser) { + this.relatedUser = relatedUser; + } + + public SimpleMeta getUserRelationshipMeta() { + return userRelationshipMeta; + } + + public void setUserRelationshipMeta(SimpleMeta userRelationshipMeta) { + this.userRelationshipMeta = userRelationshipMeta; + } + + public Links getUserRelationshipLinks() { + return userRelationshipLinks; + } + + public void setUserRelationshipLinks(Links userRelationshipLinks) { + this.userRelationshipLinks = userRelationshipLinks; + } + + public Links getLinks() { + return links; + } + + public void setLinks(Links links) { + this.links = links; + } + + @Override + public String toString() { + return "Status{" + + "id='" + id + '\'' + + ", content='" + content + '\'' + + ", commentCount=" + commentCount + + ", likeCount=" + likeCount + + ", user=" + user + + '}'; + } +} diff --git a/src/test/resources/articles-polymorph.json b/src/test/resources/articles-polymorph.json new file mode 100644 index 0000000..b6c163b --- /dev/null +++ b/src/test/resources/articles-polymorph.json @@ -0,0 +1,71 @@ +{ + "data": [{ + "type": "articles-p", + "id": "1", + "attributes": { + "title": "JSON API paints my bikeshed!" + }, + "links": { + "self": "http://example.com/articles/1" + }, + "relationships": { + "author": { + "links": { + "self": "http://example.com/articles/1/relationships/author", + "related": "http://example.com/articles/1/author" + }, + "data": { "type": "people", "id": "9" } + }, + "comments": { + "links": { + "self": "http://example.com/articles/1/relationships/comments", + "related": "http://example.com/articles/1/comments" + }, + "data": [ + { "type": "comments", "id": "5" }, + { "type": "comments", "id": "12" } + ] + } + } + }], + "included": [{ + "type": "people", + "id": "9", + "attributes": { + "first-name": "Dan", + "last-name": "Gebhardt", + "twitter": "dgeb" + }, + "links": { + "self": "http://example.com/people/9" + } + }, { + "type": "comments", + "id": "5", + "attributes": { + "body": "First!" + }, + "relationships": { + "author": { + "data": { "type": "people", "id": "2" } + } + }, + "links": { + "self": "http://example.com/comments/5" + } + }, { + "type": "comments", + "id": "12", + "attributes": { + "body": "I like XML better" + }, + "relationships": { + "author": { + "data": { "type": "people", "id": "9" } + } + }, + "links": { + "self": "http://example.com/comments/12" + } + }] +} diff --git a/src/test/resources/status-polymorph.json b/src/test/resources/status-polymorph.json new file mode 100644 index 0000000..0c7fd92 --- /dev/null +++ b/src/test/resources/status-polymorph.json @@ -0,0 +1,48 @@ +{ + "data": { + "type": "statuses-p", + "id": "id", + "attributes": { + "content": "content", + "commentCount": 1, + "likeCount": 10 + }, + "relationships": { + "user": { + "links": { + "self": "users/userid" + }, + "data": { + "type": "users", + "id": "userid" + }, + "meta" : { + "token" : "token" + } + } + } + }, + "included": [ + { + "type": "users", + "id": "userid", + "attributes": { + "name": "john" + }, + "relationships": { + "statuses-p": { + "data": [ + { + "type": "statuses-p", + "id": "id" + }, + { + "type": "statuses-p", + "id": "anotherid" + } + ] + } + } + } + ] +} From 035f02ae3e1155aedcf5e836bcf49da622f19269 Mon Sep 17 00:00:00 2001 From: dulleh Date: Fri, 11 Aug 2017 15:42:59 +0100 Subject: [PATCH 5/6] Fix class not registered error (for polymorph relationship) --- .../jsonapi/ConverterConfiguration.java | 18 +++++++--- .../jasminb/jsonapi/ResourceConverter.java | 33 ++++++++++++------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java b/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java index d1e2356..0392ede 100644 --- a/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java +++ b/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java @@ -22,7 +22,7 @@ */ public class ConverterConfiguration { - private final Map> typeToClassMapping = new HashMap<>(); + private final Map>> typeToClassesMapping = new HashMap<>(); private final Map, Type> typeAnnotations = new HashMap<>(); private final Map, Field> idMap = new HashMap<>(); private final Map, ResourceIdHandler> idHandlerMap = new HashMap<>(); @@ -58,7 +58,14 @@ public ConverterConfiguration(Class... classes) { private void processClass(Class clazz) { if (clazz.isAnnotationPresent(Type.class)) { Type annotation = clazz.getAnnotation(Type.class); - typeToClassMapping.put(annotation.value(), clazz); + + List> listOfClassesForType = typeToClassesMapping.get(annotation.value()); + if (listOfClassesForType == null) { + listOfClassesForType = new ArrayList<>(); + } + listOfClassesForType.add(clazz); + typeToClassesMapping.put(annotation.value(), listOfClassesForType); + typeAnnotations.put(clazz, annotation); relationshipTypeMap.put(clazz, new HashMap>()); relationshipFieldMap.put(clazz, new HashMap()); @@ -108,6 +115,7 @@ private void processClass(Class clazz) { //handle polymorphic relationships for (Field polymorphRelationshipField : polymorphRelationshipFields) { polymorphRelationshipField.setAccessible(true); + PolymorphRelationship relationship = polymorphRelationshipField.getAnnotation(PolymorphRelationship.class); Class targetType = ReflectionUtils.getFieldType(polymorphRelationshipField); @@ -265,12 +273,12 @@ public Field getLinksField(Class clazz) { } /** - * Resolves a type for given type name. + * Resolves the possible types for given a type name. * @param typeName {@link String} type name * @return {@link Class} resolved type */ - public Class getTypeClass(String typeName) { - return typeToClassMapping.get(typeName); + public List> getTypeClass(String typeName) { + return typeToClassesMapping.get(typeName); } /** diff --git a/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java b/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java index 0a27285..8d26304 100644 --- a/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java +++ b/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java @@ -412,15 +412,18 @@ private Map getIncludedResources(JsonNode parent) 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.put(createIdentifier(jsonNode), object); + List> classes = configuration.getTypeClass(type); + if (classes != null) { + for (Class clazz : classes) { + if (clazz != null) { + Object object = readObject(jsonNode, clazz, false); + if (object != null) { + result.put(createIdentifier(jsonNode), object); + } + } else if (!deserializationFeatures.contains(DeserializationFeature.ALLOW_UNKNOWN_INCLUSIONS)) { + throw new IllegalArgumentException("Included section contains unknown resource type: " + type); + } } - } else if (!deserializationFeatures.contains(DeserializationFeature.ALLOW_UNKNOWN_INCLUSIONS)) { - throw new IllegalArgumentException("Included section contains unknown resource type: " + type); } } } @@ -1099,12 +1102,18 @@ private Class getActualType(JsonNode object, Class userType) { String definedTypeName = configuration.getTypeName(userType); if (definedTypeName != null && definedTypeName.equals(type)) { + System.out.println("first branch " + type); return userType; } else { - Class actualType = configuration.getTypeClass(type); - - if (actualType != null && userType.isAssignableFrom(actualType)) { - return actualType; + System.out.println("second branch " + type); + List> actualTypes = configuration.getTypeClass(type); + if (actualTypes != null) { + for (Class actualType : actualTypes) { + if (actualType != null && userType.isAssignableFrom(actualType)) { + System.out.println("third branch " + type); + return actualType; + } + } } } From 406a42019a880efa248a13a899f58e18e44f748c Mon Sep 17 00:00:00 2001 From: dulleh Date: Fri, 11 Aug 2017 20:21:12 +0100 Subject: [PATCH 6/6] Fix relType not being checked against polymorph relationships --- .../github/jasminb/jsonapi/ConverterConfiguration.java | 1 - .../com/github/jasminb/jsonapi/ResourceConverter.java | 4 +++- .../github/jasminb/jsonapi/ResourceConverterTest.java | 10 ++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java b/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java index 0392ede..ed6d2ab 100644 --- a/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java +++ b/src/main/java/com/github/jasminb/jsonapi/ConverterConfiguration.java @@ -201,7 +201,6 @@ private void processClass(Class clazz) { } else { throw new IllegalArgumentException("Only single @Id annotation is allowed per defined type!"); } - } // Collecting Meta fields diff --git a/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java b/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java index 8d26304..0b04995 100644 --- a/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java +++ b/src/main/java/com/github/jasminb/jsonapi/ResourceConverter.java @@ -496,7 +496,6 @@ private void handleRelationships(JsonNode source, Object object) } // Get resolve flag - //TODO: add to fieldRelationship regardless of polymorph or not (getFieldRelationship needs to be used for either) Relationship classRelationship = configuration.getFieldRelationship(relationshipField); boolean resolveRelationship; if (classRelationship == null) { @@ -509,6 +508,9 @@ private void handleRelationships(JsonNode source, Object object) // Use resolver if possible if (resolveRelationship && resolver != null && relationship.has(LINKS)) { String relType = configuration.getFieldRelationship(relationshipField).relType().getRelName(); + //if the relType was not defined using @Relationship annotations, check for @PolymorphRelationship + if (relType == null) relType = configuration.getFieldPolymorphRelationship(relationshipField).relType().getRelName(); + JsonNode linkNode = relationship.get(LINKS).get(relType); String link; diff --git a/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java b/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java index 0a4e6a4..6ec7411 100644 --- a/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java +++ b/src/test/java/com/github/jasminb/jsonapi/ResourceConverterTest.java @@ -25,10 +25,7 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Testing functionality of JSON API converter. @@ -169,6 +166,10 @@ public void testReadWriteObjectWithPolymorph() throws Exception { status.getUser().setId("userid"); status.setRelatedUser(status.getUser()); + Map userRelationshipLinks = new HashMap<>(); + userRelationshipLinks.put("first", new Link("test.com")); + status.setUserRelationshipLinks(new Links(userRelationshipLinks)); + byte [] rawData = converter.writeDocument(new JSONAPIDocument<>(status)); Assert.assertNotNull(rawData); @@ -176,6 +177,7 @@ public void testReadWriteObjectWithPolymorph() throws Exception { JSONAPIDocument convertedDocument = converter.readDocument(new ByteArrayInputStream(rawData), StatusPolymorph.class); StatusPolymorph converted = convertedDocument.get(); + // Make sure relationship with disabled serialisation is not present Assert.assertNull(converted.getRelatedUser());