From b2ace5fc34fd7db72c5c8e45c63ac2241be79387 Mon Sep 17 00:00:00 2001 From: Gregory Fernandez Date: Wed, 5 Oct 2022 18:01:58 +0200 Subject: [PATCH 1/2] Wrong type in relationships section when using lookup by id and resource is a subtype. --- .../mapper/IncludeRelationshipLoader.java | 17 ++++ .../java/io/crnk/core/CoreTestModule.java | 2 + .../SubTypedRelationIdLookupTest.java | 89 +++++++++++++++++++ .../io/crnk/core/mock/models/BottomTask.java | 27 ++++++ .../io/crnk/core/mock/models/MiddleTask.java | 27 ++++++ .../mock/models/RelationIdTestResource.java | 26 +++++- .../io/crnk/core/mock/models/TopTask.java | 40 +++++++++ .../mock/repository/TopTaskRepository.java | 34 +++++++ 8 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 crnk-core/src/test/java/io/crnk/core/engine/internal/document/mapper/lookup/relationid/SubTypedRelationIdLookupTest.java create mode 100644 crnk-core/src/test/java/io/crnk/core/mock/models/BottomTask.java create mode 100644 crnk-core/src/test/java/io/crnk/core/mock/models/MiddleTask.java create mode 100644 crnk-core/src/test/java/io/crnk/core/mock/models/TopTask.java create mode 100644 crnk-core/src/test/java/io/crnk/core/mock/repository/TopTaskRepository.java diff --git a/crnk-core/src/main/java/io/crnk/core/engine/internal/document/mapper/IncludeRelationshipLoader.java b/crnk-core/src/main/java/io/crnk/core/engine/internal/document/mapper/IncludeRelationshipLoader.java index d0cc71edd..4ab9a127b 100644 --- a/crnk-core/src/main/java/io/crnk/core/engine/internal/document/mapper/IncludeRelationshipLoader.java +++ b/crnk-core/src/main/java/io/crnk/core/engine/internal/document/mapper/IncludeRelationshipLoader.java @@ -15,6 +15,7 @@ import io.crnk.core.engine.registry.ResourceRegistry; import io.crnk.core.engine.result.Result; import io.crnk.core.engine.result.ResultFactory; +import io.crnk.core.exception.InvalidResourceException; import io.crnk.core.exception.RepositoryNotFoundException; import io.crnk.core.exception.ResourceNotFoundException; import io.crnk.core.repository.response.JsonApiResponse; @@ -26,6 +27,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -127,6 +129,7 @@ public Result> lookupRelatedResourcesWithId(IncludeRequest request Set related = new HashSet<>(); Set relatedIdsToLoad = new HashSet<>(); + Map mapRelatedIdsToLoadToResourceIdentifier = new HashMap<>(); for (Resource sourceResource : sourceResources) { Relationship relationship = sourceResource.getRelationships().get(relationshipField.getJsonName()); PreconditionUtil.verify(relationship.getData().isPresent(), "expected relationship data to be loaded for @JsonApiResourceId annotated field, sourceType=%d sourceId=%d, relationshipName=%s", sourceResource.getType(), sourceResource.getId(), relationshipField.getJsonName()); @@ -138,6 +141,9 @@ public Result> lookupRelatedResourcesWithId(IncludeRequest request related.add(request.getResource(id)); } else { relatedIdsToLoad.add(oppositeResourceInformation.parseIdString(id.getId())); + // ResourceIdentifier may have the wrong type, e.g. when resource is a subtype of declared type in source resource, + // so we store the resource identifier to be able to set the correct type later + mapRelatedIdsToLoadToResourceIdentifier.put(id.getId(), id); } } } @@ -153,6 +159,17 @@ public Result> lookupRelatedResourcesWithId(IncludeRequest request Collection responseList = (Collection) response.getEntity(); for (Object responseEntity : responseList) { Resource relatedResource = request.merge(responseEntity); + // ResourceIdentifier may have the wrong type, e.g. when resource is a subtype of declared type in source resource, + // so we set the correct type here + ResourceIdentifier resourceIdentifier = mapRelatedIdsToLoadToResourceIdentifier.get(relatedResource.getId()); + if (resourceIdentifier != null) { + if (resourceIdentifier.getType() != relatedResource.getType()) { + resourceIdentifier.setType(relatedResource.getType()); + } + } else { + throw new InvalidResourceException("type=" + relationshipField.getOppositeResourceType() + ", " + + "id=" + relatedResource.getId() + " : There must be an issue with serializing this id."); + } related.add(relatedResource); Object responseEntityId = oppositeResourceInformation.getId(responseEntity); relatedIdsToLoad.remove(responseEntityId); diff --git a/crnk-core/src/test/java/io/crnk/core/CoreTestModule.java b/crnk-core/src/test/java/io/crnk/core/CoreTestModule.java index 67d551310..e097b631e 100644 --- a/crnk-core/src/test/java/io/crnk/core/CoreTestModule.java +++ b/crnk-core/src/test/java/io/crnk/core/CoreTestModule.java @@ -14,6 +14,7 @@ import io.crnk.core.mock.repository.RelationIdTestRepository; import io.crnk.core.mock.repository.RelationshipBehaviorTestRepository; import io.crnk.core.mock.repository.ScheduleRepositoryImpl; +import io.crnk.core.mock.repository.TopTaskRepository; import io.crnk.core.mock.repository.TaskRepository; import io.crnk.core.mock.repository.TaskToProjectRepository; import io.crnk.core.mock.repository.TaskWithLookupRepository; @@ -43,6 +44,7 @@ public void setupModule(ModuleContext context) { context.addRepository(new TaskToProjectRepository()); context.addRepository(new TaskWithLookupRepository()); context.addRepository(new TaskWithLookupToProjectRepository()); + context.addRepository(new TopTaskRepository()); context.addRepository(new UserRepository()); context.addRepository(new UserToProjectRepository()); context.addRepository(new UserToTaskRepository()); diff --git a/crnk-core/src/test/java/io/crnk/core/engine/internal/document/mapper/lookup/relationid/SubTypedRelationIdLookupTest.java b/crnk-core/src/test/java/io/crnk/core/engine/internal/document/mapper/lookup/relationid/SubTypedRelationIdLookupTest.java new file mode 100644 index 000000000..f25e4db16 --- /dev/null +++ b/crnk-core/src/test/java/io/crnk/core/engine/internal/document/mapper/lookup/relationid/SubTypedRelationIdLookupTest.java @@ -0,0 +1,89 @@ +package io.crnk.core.engine.internal.document.mapper.lookup.relationid; + +import java.util.Arrays; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import io.crnk.core.engine.document.Document; +import io.crnk.core.engine.document.Resource; +import io.crnk.core.engine.document.ResourceIdentifier; +import io.crnk.core.engine.internal.document.mapper.AbstractDocumentMapperTest; +import io.crnk.core.mock.models.BottomTask; +import io.crnk.core.mock.models.RelationIdTestResource; +import io.crnk.core.mock.models.TopTask; +import io.crnk.core.mock.repository.TopTaskRepository; +import io.crnk.core.queryspec.QuerySpec; +import io.crnk.core.repository.ResourceRepository; +import io.crnk.core.utils.Nullable; + +public class SubTypedRelationIdLookupTest extends AbstractDocumentMapperTest { + + + private TopTaskRepository topTaskRepository; + + private BottomTask bottomTask; + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Before + public void setup() { + super.setup(); + + topTaskRepository = (TopTaskRepository) (ResourceRepository) container.getRepository(TopTask.class); + bottomTask = new BottomTask(); + bottomTask.setId(3L); + bottomTask.setName("test"); + topTaskRepository.save(bottomTask); + + } + + @Test + public void checkOnlyIdSet() { + check(false, true); + } + + @Test + public void checkNull() { + check(false, false); + } + + @Test + public void checkEntitySet() { + check(true, true); + } + + private void check(boolean setRelatedEntity, boolean setRelatedId) { + RelationIdTestResource entity = new RelationIdTestResource(); + entity.setId(2L); + entity.setName("test"); + if (setRelatedId) { + entity.setTestSubTypedResourceId(3L); + } + if (setRelatedEntity) { + entity.setTestSubTypedResource(bottomTask); + } + + QuerySpec querySpec = new QuerySpec(RelationIdTestResource.class); + querySpec.includeRelation(Arrays.asList("testSubTypedResource")); + + Document document = mapper.toDocument(toResponse(entity), toAdapter(querySpec), mappingConfig).get(); + Resource resource = document.getSingleData().get(); + Assert.assertEquals("2", resource.getId()); + Assert.assertEquals("relationIdTest", resource.getType()); + Assert.assertEquals("test", resource.getAttributes().get("name").asText()); + + Nullable data = resource.getRelationships().get("testSubTypedResource").getSingleData(); + Assert.assertTrue(data.isPresent()); + + if (setRelatedId) { + Assert.assertNotNull(data.get()); + Assert.assertEquals(1, document.getIncluded().size()); + Assert.assertEquals("3", document.getIncluded().get(0).getId()); + Assert.assertEquals("bottomTask", document.getIncluded().get(0).getType()); + Assert.assertEquals("bottomTask", data.get().getType()); + } else { + Assert.assertNull(data.get()); + } + } +} diff --git a/crnk-core/src/test/java/io/crnk/core/mock/models/BottomTask.java b/crnk-core/src/test/java/io/crnk/core/mock/models/BottomTask.java new file mode 100644 index 000000000..5537001b7 --- /dev/null +++ b/crnk-core/src/test/java/io/crnk/core/mock/models/BottomTask.java @@ -0,0 +1,27 @@ +package io.crnk.core.mock.models; + +import io.crnk.core.resource.annotations.JsonApiResource; + +@JsonApiResource(type = "bottomTask", resourcePath = "treeTasks") +public class BottomTask extends MiddleTask { + + private boolean recurring; + + private String end; + + public boolean isRecurring() { + return recurring; + } + + public void setRecurring(final boolean recurring) { + this.recurring = recurring; + } + + public String getEnd() { + return end; + } + + public void setEnd(final String end) { + this.end = end; + } +} \ No newline at end of file diff --git a/crnk-core/src/test/java/io/crnk/core/mock/models/MiddleTask.java b/crnk-core/src/test/java/io/crnk/core/mock/models/MiddleTask.java new file mode 100644 index 000000000..099cd2c65 --- /dev/null +++ b/crnk-core/src/test/java/io/crnk/core/mock/models/MiddleTask.java @@ -0,0 +1,27 @@ +package io.crnk.core.mock.models; + +import io.crnk.core.resource.annotations.JsonApiResource; + +@JsonApiResource(type = "middleTask", subTypes = BottomTask.class, resourcePath = "treeTasks") +public abstract class MiddleTask extends TopTask { + + private String publicComment; + + private String privateComment; + + public String getPublicComment() { + return publicComment; + } + + public void setPublicComment(final String publicComment) { + this.publicComment = publicComment; + } + + public String getPrivateComment() { + return privateComment; + } + + public void setPrivateComment(final String privateComment) { + this.privateComment = privateComment; + } +} \ No newline at end of file diff --git a/crnk-core/src/test/java/io/crnk/core/mock/models/RelationIdTestResource.java b/crnk-core/src/test/java/io/crnk/core/mock/models/RelationIdTestResource.java index ee30d2baf..932ecad4e 100644 --- a/crnk-core/src/test/java/io/crnk/core/mock/models/RelationIdTestResource.java +++ b/crnk-core/src/test/java/io/crnk/core/mock/models/RelationIdTestResource.java @@ -38,7 +38,6 @@ public class RelationIdTestResource { @JsonApiRelation(lookUp = LookupIncludeBehavior.AUTOMATICALLY_WHEN_NULL) private Schedule testLookupWhenNull; - @JsonApiRelationId private List testMultipleValueIds = new ArrayList<>(); @@ -81,6 +80,12 @@ public class RelationIdTestResource { @JsonApiRelation(lookUp = LookupIncludeBehavior.AUTOMATICALLY_WHEN_NULL) private Schedule testResourceIdRef; + @JsonApiRelationId + private Long testSubTypedResourceId; + + @JsonApiRelation(lookUp = LookupIncludeBehavior.AUTOMATICALLY_WHEN_NULL) + private TopTask testSubTypedResource; + public Long getId() { return id; } @@ -273,4 +278,23 @@ public void setTestResourceIdRef(Schedule testResourceIdRef) { this.testResourceIdRefId = testResourceIdRef != null ? new ResourceIdentifier(testResourceIdRef.getId().toString(), "schedules") : null; } + + public Long getTestSubTypedResourceId() { + return testSubTypedResourceId; + } + + public void setTestSubTypedResourceId(final Long testSubTypedResourceId) { + this.testSubTypedResourceId = testSubTypedResourceId; + this.testSubTypedResource = null; + } + + public TopTask getTestSubTypedResource() { + return testSubTypedResource; + } + + public void setTestSubTypedResource(final TopTask testSubTypedResource) { + this.testSubTypedResource = testSubTypedResource; + this.testSubTypedResourceId = testSubTypedResource != null ? testSubTypedResource.getId() : null; + } + } diff --git a/crnk-core/src/test/java/io/crnk/core/mock/models/TopTask.java b/crnk-core/src/test/java/io/crnk/core/mock/models/TopTask.java new file mode 100644 index 000000000..bb8a77df0 --- /dev/null +++ b/crnk-core/src/test/java/io/crnk/core/mock/models/TopTask.java @@ -0,0 +1,40 @@ +package io.crnk.core.mock.models; + +import io.crnk.core.resource.annotations.JsonApiId; +import io.crnk.core.resource.annotations.JsonApiResource; + +@JsonApiResource(type = "topTask", subTypes = MiddleTask.class, resourcePath = "treeTasks") +public abstract class TopTask { + + @JsonApiId + private Long id; + + private String name; + + private String category; + + public Long getId() { + return id; + } + + public TopTask setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public void setName(@SuppressWarnings("SameParameterValue") String name) { + this.name = name; + } + + public String getCategory() { + return category; + } + + public void setCategory(final String category) { + this.category = category; + } +} diff --git a/crnk-core/src/test/java/io/crnk/core/mock/repository/TopTaskRepository.java b/crnk-core/src/test/java/io/crnk/core/mock/repository/TopTaskRepository.java new file mode 100644 index 000000000..9e60fb7cd --- /dev/null +++ b/crnk-core/src/test/java/io/crnk/core/mock/repository/TopTaskRepository.java @@ -0,0 +1,34 @@ +package io.crnk.core.mock.repository; + +import java.util.HashMap; +import java.util.Map; + +import io.crnk.core.mock.models.TopTask; +import io.crnk.core.queryspec.QuerySpec; +import io.crnk.core.repository.ResourceRepositoryBase; +import io.crnk.core.resource.list.ResourceList; + +public class TopTaskRepository extends ResourceRepositoryBase { + + private Map tasks = new HashMap<>(); + + public TopTaskRepository() { + super(TopTask.class); + } + + @Override + public ResourceList findAll(final QuerySpec querySpec) { + return querySpec.apply(tasks.values()); + } + + @Override + public S save(S entity) { + tasks.put(entity.getId(), entity); + return entity; + } + + @Override + public void delete(Long id) { + tasks.remove(id); + } +} \ No newline at end of file From 6b88d3ce2d679a9ecadd1305acb670c77799f494 Mon Sep 17 00:00:00 2001 From: Gregory Fernandez Date: Fri, 7 Oct 2022 17:19:09 +0200 Subject: [PATCH 2/2] Complexify test case to have multiple time the same relationship in a collection. Fix code. --- .../mapper/IncludeRelationshipLoader.java | 10 +- .../java/io/crnk/core/CoreTestModule.java | 2 + .../SubTypedRelationIdLookupTest.java | 96 +++++++++++++++---- .../mock/models/RelationIdTestResource.java | 10 ++ .../crnk/core/mock/models/TopTaskWrapper.java | 46 +++++++++ .../repository/TopTaskWrapperRepository.java | 11 +++ 6 files changed, 152 insertions(+), 23 deletions(-) create mode 100644 crnk-core/src/test/java/io/crnk/core/mock/models/TopTaskWrapper.java create mode 100644 crnk-core/src/test/java/io/crnk/core/mock/repository/TopTaskWrapperRepository.java diff --git a/crnk-core/src/main/java/io/crnk/core/engine/internal/document/mapper/IncludeRelationshipLoader.java b/crnk-core/src/main/java/io/crnk/core/engine/internal/document/mapper/IncludeRelationshipLoader.java index 4ab9a127b..e8d5a38e0 100644 --- a/crnk-core/src/main/java/io/crnk/core/engine/internal/document/mapper/IncludeRelationshipLoader.java +++ b/crnk-core/src/main/java/io/crnk/core/engine/internal/document/mapper/IncludeRelationshipLoader.java @@ -129,7 +129,7 @@ public Result> lookupRelatedResourcesWithId(IncludeRequest request Set related = new HashSet<>(); Set relatedIdsToLoad = new HashSet<>(); - Map mapRelatedIdsToLoadToResourceIdentifier = new HashMap<>(); + Map> mapRelatedIdsToLoadToResourceIdentifier = new HashMap<>(); for (Resource sourceResource : sourceResources) { Relationship relationship = sourceResource.getRelationships().get(relationshipField.getJsonName()); PreconditionUtil.verify(relationship.getData().isPresent(), "expected relationship data to be loaded for @JsonApiResourceId annotated field, sourceType=%d sourceId=%d, relationshipName=%s", sourceResource.getType(), sourceResource.getId(), relationshipField.getJsonName()); @@ -143,7 +143,7 @@ public Result> lookupRelatedResourcesWithId(IncludeRequest request relatedIdsToLoad.add(oppositeResourceInformation.parseIdString(id.getId())); // ResourceIdentifier may have the wrong type, e.g. when resource is a subtype of declared type in source resource, // so we store the resource identifier to be able to set the correct type later - mapRelatedIdsToLoadToResourceIdentifier.put(id.getId(), id); + mapRelatedIdsToLoadToResourceIdentifier.computeIfAbsent(id.getId(), k -> new ArrayList<>()).add(id); } } } @@ -161,9 +161,9 @@ public Result> lookupRelatedResourcesWithId(IncludeRequest request Resource relatedResource = request.merge(responseEntity); // ResourceIdentifier may have the wrong type, e.g. when resource is a subtype of declared type in source resource, // so we set the correct type here - ResourceIdentifier resourceIdentifier = mapRelatedIdsToLoadToResourceIdentifier.get(relatedResource.getId()); - if (resourceIdentifier != null) { - if (resourceIdentifier.getType() != relatedResource.getType()) { + List resourceIdentifiers = mapRelatedIdsToLoadToResourceIdentifier.get(relatedResource.getId()); + if (resourceIdentifiers != null) { + for(ResourceIdentifier resourceIdentifier : resourceIdentifiers) { resourceIdentifier.setType(relatedResource.getType()); } } else { diff --git a/crnk-core/src/test/java/io/crnk/core/CoreTestModule.java b/crnk-core/src/test/java/io/crnk/core/CoreTestModule.java index e097b631e..71160ddce 100644 --- a/crnk-core/src/test/java/io/crnk/core/CoreTestModule.java +++ b/crnk-core/src/test/java/io/crnk/core/CoreTestModule.java @@ -20,6 +20,7 @@ import io.crnk.core.mock.repository.TaskWithLookupRepository; import io.crnk.core.mock.repository.TaskWithLookupToProjectRepository; import io.crnk.core.mock.repository.ThingRepository; +import io.crnk.core.mock.repository.TopTaskWrapperRepository; import io.crnk.core.mock.repository.UserRepository; import io.crnk.core.mock.repository.UserToProjectRepository; import io.crnk.core.mock.repository.UserToTaskRepository; @@ -45,6 +46,7 @@ public void setupModule(ModuleContext context) { context.addRepository(new TaskWithLookupRepository()); context.addRepository(new TaskWithLookupToProjectRepository()); context.addRepository(new TopTaskRepository()); + context.addRepository(new TopTaskWrapperRepository()); context.addRepository(new UserRepository()); context.addRepository(new UserToProjectRepository()); context.addRepository(new UserToTaskRepository()); diff --git a/crnk-core/src/test/java/io/crnk/core/engine/internal/document/mapper/lookup/relationid/SubTypedRelationIdLookupTest.java b/crnk-core/src/test/java/io/crnk/core/engine/internal/document/mapper/lookup/relationid/SubTypedRelationIdLookupTest.java index f25e4db16..4d9f8cd92 100644 --- a/crnk-core/src/test/java/io/crnk/core/engine/internal/document/mapper/lookup/relationid/SubTypedRelationIdLookupTest.java +++ b/crnk-core/src/test/java/io/crnk/core/engine/internal/document/mapper/lookup/relationid/SubTypedRelationIdLookupTest.java @@ -1,6 +1,10 @@ package io.crnk.core.engine.internal.document.mapper.lookup.relationid; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Before; @@ -13,6 +17,7 @@ import io.crnk.core.mock.models.BottomTask; import io.crnk.core.mock.models.RelationIdTestResource; import io.crnk.core.mock.models.TopTask; +import io.crnk.core.mock.models.TopTaskWrapper; import io.crnk.core.mock.repository.TopTaskRepository; import io.crnk.core.queryspec.QuerySpec; import io.crnk.core.repository.ResourceRepository; @@ -20,10 +25,21 @@ public class SubTypedRelationIdLookupTest extends AbstractDocumentMapperTest { + public static final Long TEST_RESOURCE_ID = 1L; + public static final Long TASK_1_ID = 2L; + public static final Long TASK_2_ID = 3L; + public static final Long TASK_WRAPPER_1_ID = 4L; + public static final Long TASK_WRAPPER_2_ID = 5L; private TopTaskRepository topTaskRepository; - private BottomTask bottomTask; + private BottomTask bottomTask1; + + private BottomTask bottomTask2; + + private TopTaskWrapper topTaskWrapper1; + + private TopTaskWrapper topTaskWrapper2; @SuppressWarnings({"rawtypes", "unchecked"}) @Before @@ -31,11 +47,22 @@ public void setup() { super.setup(); topTaskRepository = (TopTaskRepository) (ResourceRepository) container.getRepository(TopTask.class); - bottomTask = new BottomTask(); - bottomTask.setId(3L); - bottomTask.setName("test"); - topTaskRepository.save(bottomTask); + bottomTask1 = createTask(TASK_1_ID); + topTaskRepository.save(bottomTask1); + + bottomTask2 = createTask(TASK_2_ID); + topTaskRepository.save(bottomTask2); + + topTaskWrapper1 = createTopTaskWrapper(TASK_WRAPPER_1_ID); + topTaskWrapper2 = createTopTaskWrapper(TASK_WRAPPER_2_ID); + } + + private BottomTask createTask(final Long id) { + BottomTask task = new BottomTask(); + task.setId(id); + task.setName("test" + id); + return task; } @Test @@ -55,35 +82,68 @@ public void checkEntitySet() { private void check(boolean setRelatedEntity, boolean setRelatedId) { RelationIdTestResource entity = new RelationIdTestResource(); - entity.setId(2L); + entity.setId(TEST_RESOURCE_ID); entity.setName("test"); + entity.setTopTaskWrappers(Arrays.asList(topTaskWrapper1, topTaskWrapper2)); if (setRelatedId) { - entity.setTestSubTypedResourceId(3L); + entity.setTestSubTypedResourceId(TASK_1_ID); + topTaskWrapper1.setTaskId(bottomTask2.getId()); + topTaskWrapper2.setTaskId(bottomTask2.getId()); } if (setRelatedEntity) { - entity.setTestSubTypedResource(bottomTask); + entity.setTestSubTypedResource(bottomTask1); + topTaskWrapper1.setTask(bottomTask2); + topTaskWrapper2.setTask(bottomTask2); } QuerySpec querySpec = new QuerySpec(RelationIdTestResource.class); - querySpec.includeRelation(Arrays.asList("testSubTypedResource")); + querySpec.includeRelation(Collections.singletonList("testSubTypedResource")); + querySpec.includeRelation(Arrays.asList("topTaskWrappers", "task")); Document document = mapper.toDocument(toResponse(entity), toAdapter(querySpec), mappingConfig).get(); Resource resource = document.getSingleData().get(); - Assert.assertEquals("2", resource.getId()); + Assert.assertEquals(TEST_RESOURCE_ID.toString(), resource.getId()); Assert.assertEquals("relationIdTest", resource.getType()); Assert.assertEquals("test", resource.getAttributes().get("name").asText()); - Nullable data = resource.getRelationships().get("testSubTypedResource").getSingleData(); - Assert.assertTrue(data.isPresent()); + Nullable testSubTypedResourceId = resource.getRelationships().get("testSubTypedResource").getSingleData(); + Assert.assertTrue(testSubTypedResourceId.isPresent()); + Nullable> topTaskWrappersIds = resource.getRelationships().get("topTaskWrappers").getCollectionData(); + Assert.assertTrue(topTaskWrappersIds.isPresent()); + Assert.assertNotNull(topTaskWrappersIds.get()); + List topTaskWrappers = document.getIncluded().stream().filter(it -> it.getType().equals("topTaskWrapper")).collect(Collectors.toList()); + Assert.assertEquals(2, topTaskWrappers.size()); if (setRelatedId) { - Assert.assertNotNull(data.get()); - Assert.assertEquals(1, document.getIncluded().size()); - Assert.assertEquals("3", document.getIncluded().get(0).getId()); - Assert.assertEquals("bottomTask", document.getIncluded().get(0).getType()); - Assert.assertEquals("bottomTask", data.get().getType()); + Assert.assertNotNull(testSubTypedResourceId.get()); + Assert.assertEquals("bottomTask", testSubTypedResourceId.get().getType()); + Assert.assertEquals(TASK_1_ID.toString(), testSubTypedResourceId.get().getId()); + + List taskIds = topTaskWrappers.stream().map(it -> it.getRelationships().get("task").getSingleData().get()).collect(Collectors.toList()); + Assert.assertEquals(2, findResourceIdentifierByTypeAndId(taskIds, "bottomTask", TASK_2_ID).size()); + Assert.assertEquals(0, findResourceIdentifierByTypeAndId(taskIds, "topTask", TASK_2_ID).size()); + + Assert.assertEquals(4, document.getIncluded().size()); + Assert.assertEquals(1, findIncludedByTypeAndId(document.getIncluded(), "bottomTask", TASK_1_ID).size()); + Assert.assertEquals(0, findIncludedByTypeAndId(document.getIncluded(), "topTask", TASK_1_ID).size()); + Assert.assertEquals(1, findIncludedByTypeAndId(document.getIncluded(), "bottomTask", TASK_2_ID).size()); + Assert.assertEquals(0, findIncludedByTypeAndId(document.getIncluded(), "topTask", TASK_2_ID).size()); } else { - Assert.assertNull(data.get()); + Assert.assertNull(testSubTypedResourceId.get()); } } + + private Collection findIncludedByTypeAndId(final List included, final String type, final Long id) { + return included.stream().filter(i -> i.getId().equals(id.toString()) && i.getType().equals(type)).collect(Collectors.toList()); + } + + private Collection findResourceIdentifierByTypeAndId(final List ids, final String type, final Long id) { + return ids.stream().filter(i -> i.getId().equals(id.toString()) && i.getType().equals(type)).collect(Collectors.toList()); + } + + private TopTaskWrapper createTopTaskWrapper(final long id) { + final TopTaskWrapper topTaskWrapper = new TopTaskWrapper(); + topTaskWrapper.setId(id); + return topTaskWrapper; + } } diff --git a/crnk-core/src/test/java/io/crnk/core/mock/models/RelationIdTestResource.java b/crnk-core/src/test/java/io/crnk/core/mock/models/RelationIdTestResource.java index 932ecad4e..edfc0324b 100644 --- a/crnk-core/src/test/java/io/crnk/core/mock/models/RelationIdTestResource.java +++ b/crnk-core/src/test/java/io/crnk/core/mock/models/RelationIdTestResource.java @@ -86,6 +86,9 @@ public class RelationIdTestResource { @JsonApiRelation(lookUp = LookupIncludeBehavior.AUTOMATICALLY_WHEN_NULL) private TopTask testSubTypedResource; + @JsonApiRelation(lookUp = LookupIncludeBehavior.NONE) + private List topTaskWrappers = new ArrayList<>(); + public Long getId() { return id; } @@ -297,4 +300,11 @@ public void setTestSubTypedResource(final TopTask testSubTypedResource) { this.testSubTypedResourceId = testSubTypedResource != null ? testSubTypedResource.getId() : null; } + public List getTopTaskWrappers() { + return topTaskWrappers; + } + + public void setTopTaskWrappers(final List topTaskWrappers) { + this.topTaskWrappers = topTaskWrappers; + } } diff --git a/crnk-core/src/test/java/io/crnk/core/mock/models/TopTaskWrapper.java b/crnk-core/src/test/java/io/crnk/core/mock/models/TopTaskWrapper.java new file mode 100644 index 000000000..a7e023be9 --- /dev/null +++ b/crnk-core/src/test/java/io/crnk/core/mock/models/TopTaskWrapper.java @@ -0,0 +1,46 @@ +package io.crnk.core.mock.models; + +import io.crnk.core.resource.annotations.JsonApiId; +import io.crnk.core.resource.annotations.JsonApiRelation; +import io.crnk.core.resource.annotations.JsonApiRelationId; +import io.crnk.core.resource.annotations.JsonApiResource; +import io.crnk.core.resource.annotations.LookupIncludeBehavior; + +@JsonApiResource(type = "topTaskWrapper") +public class TopTaskWrapper { + + @JsonApiId + private Long id; + + @JsonApiRelationId + private Long taskId; + + @JsonApiRelation(lookUp = LookupIncludeBehavior.AUTOMATICALLY_WHEN_NULL) + private TopTask task; + + public Long getTaskId() { + return taskId; + } + + public void setTaskId(final Long taskId) { + this.taskId = taskId; + this.task = null; + } + + public TopTask getTask() { + return task; + } + + public void setTask(final TopTask task) { + this.task = task; + this.taskId = task != null ? task.getId() : null; + } + + public Long getId() { + return id; + } + + public void setId(final Long id) { + this.id = id; + } +} diff --git a/crnk-core/src/test/java/io/crnk/core/mock/repository/TopTaskWrapperRepository.java b/crnk-core/src/test/java/io/crnk/core/mock/repository/TopTaskWrapperRepository.java new file mode 100644 index 000000000..094ac8eaa --- /dev/null +++ b/crnk-core/src/test/java/io/crnk/core/mock/repository/TopTaskWrapperRepository.java @@ -0,0 +1,11 @@ +package io.crnk.core.mock.repository; + +import io.crnk.core.mock.models.TopTaskWrapper; +import io.crnk.core.repository.InMemoryResourceRepository; + +public class TopTaskWrapperRepository extends InMemoryResourceRepository { + + public TopTaskWrapperRepository() { + super(TopTaskWrapper.class); + } +} \ No newline at end of file