From a704e0aa8971117bf5dc480c25f0ebad1cd8050b Mon Sep 17 00:00:00 2001 From: JB Date: Mon, 7 Sep 2020 14:40:20 +0200 Subject: [PATCH] Create and update resources with modification on mappedBy relationships --- .../client/internal/ClientResourceUpsert.java | 40 ++++- .../dispatcher/controller/ResourceUpsert.java | 148 +++++++++++++++++- .../ResourcePostControllerTest.java | 10 +- 3 files changed, 187 insertions(+), 11 deletions(-) diff --git a/crnk-client/src/main/java/io/crnk/client/internal/ClientResourceUpsert.java b/crnk-client/src/main/java/io/crnk/client/internal/ClientResourceUpsert.java index 6c84c4981..bf7de1cd1 100644 --- a/crnk-client/src/main/java/io/crnk/client/internal/ClientResourceUpsert.java +++ b/crnk-client/src/main/java/io/crnk/client/internal/ClientResourceUpsert.java @@ -2,11 +2,7 @@ import java.io.IOException; import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -136,6 +132,40 @@ protected RuntimeException newBodyException(String message, IOException e) { throw new ResponseBodyException(message, e); } + @Override + protected Optional processMappedByRelationship(Object newResource, Relationship relationship, ResourceField field, Resource resource, + ResourceInformation resourceInformation, QueryAdapter queryAdapter) { + if (!relationship.getData().isPresent()) { + ObjectNode links = relationship.getLinks(); + ObjectNode meta = relationship.getMeta(); + if (links != null) { + // create proxy to lazy load relations + PreconditionUtil.verifyEquals(ResourceFieldType.RELATIONSHIP, field.getResourceFieldType(), "expected {} to be a relationship", field.getJsonName()); + Class elementType = field.getElementType(); + Class collectionClass = field.getType(); + + JsonNode relatedNode = links.get("related"); + if (relatedNode != null) { + String url = null; + if (relatedNode.has(SerializerUtil.HREF)) { + JsonNode hrefNode = relatedNode.get(SerializerUtil.HREF); + if (hrefNode != null) { + url = hrefNode.asText().trim(); + } + } else { + url = relatedNode.asText().trim(); + } + Object proxy = proxyFactory.createCollectionProxy(elementType, collectionClass, url, links, meta); + field.getAccessor().setValue(newResource, proxy); + } + } + return Optional.empty(); + } else { + // set elements + return super.processMappedByRelationship(newResource, relationship, field, resource, resourceInformation, queryAdapter); + } + } + @Override protected Optional setRelationsFieldAsync(Object newResource, RegistryEntry registryEntry, Map.Entry property, QueryAdapter queryAdapter) { diff --git a/crnk-core/src/main/java/io/crnk/core/engine/internal/dispatcher/controller/ResourceUpsert.java b/crnk-core/src/main/java/io/crnk/core/engine/internal/dispatcher/controller/ResourceUpsert.java index 41865e53e..6b2784a42 100644 --- a/crnk-core/src/main/java/io/crnk/core/engine/internal/dispatcher/controller/ResourceUpsert.java +++ b/crnk-core/src/main/java/io/crnk/core/engine/internal/dispatcher/controller/ResourceUpsert.java @@ -34,11 +34,17 @@ import io.crnk.core.exception.BadRequestException; import io.crnk.core.exception.RequestBodyException; import io.crnk.core.exception.ResourceException; +import io.crnk.core.queryspec.FilterOperator; +import io.crnk.core.queryspec.PathSpec; +import io.crnk.core.queryspec.QuerySpec; +import io.crnk.core.queryspec.internal.QuerySpecAdapter; +import io.crnk.core.queryspec.pagingspec.NumberSizePagingSpec; +import io.crnk.core.queryspec.pagingspec.OffsetLimitPagingSpec; +import io.crnk.core.queryspec.pagingspec.PagingSpec; import io.crnk.core.repository.response.JsonApiResponse; import io.crnk.core.resource.ResourceTypeHolder; import io.crnk.core.resource.list.DefaultResourceList; -import io.crnk.core.resource.meta.JsonLinksInformation; -import io.crnk.core.resource.meta.JsonMetaInformation; +import io.crnk.core.resource.meta.*; import java.io.IOException; import java.io.Serializable; @@ -52,6 +58,7 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; public abstract class ResourceUpsert extends ResourceIncludeField { @@ -246,7 +253,14 @@ protected Result setRelationsAsync(Object newResource, RegistryEntry regis ResourceFilterDirectory filterDirectory = context.getResourceFilterDirectory(); if (!checkAccess() || filterDirectory.canAccess(field, getHttpMethod(), queryAdapter.getQueryContext(), ignoreImmutableFields())) { Optional result; - if (field.isCollection()) { + if (field.isMappedBy()) { + result = processMappedByRelationship(newResource, + relationship, + field, + resource, + resourceInformation, + queryAdapter); + } else if (field.isCollection()) { //noinspection unchecked result = setRelationsFieldAsync(newResource, registryEntry, @@ -268,6 +282,134 @@ protected Result setRelationsAsync(Object newResource, RegistryEntry regis return resultFactory.zip((List) results); } + protected Optional processMappedByRelationship(Object newResource, Relationship relationship, ResourceField field, Resource resource, + ResourceInformation resourceInformation, QueryAdapter queryAdapter) { + RegistryEntry oppositeEntry = context.getResourceRegistry().getEntry(field.getOppositeResourceType()); + ResourceInformation oppositeInformation = oppositeEntry.getResourceInformation(); + ResourceField oppositeField = oppositeInformation.findRelationshipFieldByName(field.getOppositeName()); + // First get all opposite resources that refer to the current resource and those that are in the relationship ids + // Opposite resources that currently refer to this resource + Class localIdFieldType = resourceInformation.getIdField().getType(); + TypeParser typeParser = context.getTypeParser(); + Serializable localTypeId = (Serializable) typeParser.parse(resource.getId(), localIdFieldType); + QuerySpec referenceQuerySpec = new QuerySpec(oppositeInformation.getResourceType()); + PagingSpec pagingSpec = oppositeEntry.getPagingBehavior().createDefaultPagingSpec(); + referenceQuerySpec.setPaging(pagingSpec); + String referenceName = field.getOppositeName() + "." + resourceInformation.getIdField().getUnderlyingName(); + referenceQuerySpec.addFilter(PathSpec.of(referenceName).filter(FilterOperator.EQ, localTypeId)); + QueryContext queryContext = queryAdapter.getQueryContext(); + QueryAdapter referenceQueryAdapter = new QuerySpecAdapter(referenceQuerySpec, context.getResourceRegistry(), queryContext); + Result metaResult = oppositeEntry.getResourceRepository().findAll(referenceQueryAdapter).map(JsonApiResponse::getMetaInformation); + MetaInformation metaInformation = metaResult.get(); + if (metaInformation instanceof PagedMetaInformation) { + PagedMetaInformation pagedMetaInformation = (PagedMetaInformation) metaResult.get(); + if (referenceQueryAdapter.getPagingSpec() instanceof NumberSizePagingSpec) { + NumberSizePagingSpec numberSizePagingSpec = (NumberSizePagingSpec)referenceQueryAdapter.getPagingSpec(); + numberSizePagingSpec.setSize(pagedMetaInformation.getTotalResourceCount().intValue()); + } else if (referenceQueryAdapter.getPagingSpec() instanceof OffsetLimitPagingSpec) { + OffsetLimitPagingSpec offsetLimitPagingSpec = (OffsetLimitPagingSpec)referenceQueryAdapter.getPagingSpec(); + offsetLimitPagingSpec.setOffset(0); + offsetLimitPagingSpec.setLimit(pagedMetaInformation.getTotalResourceCount()); + } + } + + Result previousResult = oppositeEntry.getResourceRepository().findAll(referenceQueryAdapter).map(JsonApiResponse::getEntity); + + // Opposite resources that are in the relationship ids + List> relatedResults = new ArrayList<>(); + if (relationship.getData().isPresent()) { + LinkedList relationshipTypedIds = new LinkedList<>(); + ArrayList relationshipIds = new ArrayList<>(); + if (field.isCollection()) { + relationshipIds.addAll(relationship.getCollectionData().get()); + } else { + ResourceIdentifier relationshipId = (ResourceIdentifier) relationship.getData().get(); + if (relationshipId != null) { + relationshipIds.add(relationshipId); + } + } + + for (ResourceIdentifier resourceId : relationshipIds) { + Class idFieldType = oppositeInformation.getIdField().getType(); + Serializable typedRelationshipId = parseId(resourceId, idFieldType); + relationshipTypedIds.add(typedRelationshipId); + } + + + for (int i = 0; i < relationshipIds.size(); i++) { + Serializable typedRelationshipId = (Serializable) relationshipTypedIds.get(i); + relatedResults.add(fetchRelated(oppositeEntry, typedRelationshipId, queryAdapter)); + } + } + + QuerySpec oppositeUpdateQuerySpec = new QuerySpec(oppositeInformation.getResourceType()); + oppositeUpdateQuerySpec.setPaging(pagingSpec); + QueryAdapter oppositeQueryAdapter = new QuerySpecAdapter(oppositeUpdateQuerySpec, context.getResourceRegistry(), queryAdapter.getQueryContext()); + + DefaultResourceList oldList = (DefaultResourceList) previousResult.get(); + + DefaultResourceList newList = new DefaultResourceList<>(); + + ObjectMapper objectMapper = context.getObjectMapper(); + ObjectNode metaNode = relationship.getMeta(); + if (metaNode != null) { + newList.setMeta(new JsonMetaInformation(metaNode, objectMapper)); + } + ObjectNode linksNode = relationship.getLinks(); + if (linksNode != null) { + newList.setLinks(new JsonLinksInformation(linksNode, objectMapper)); + } + + Optional returnValue; + if (relatedResults.isEmpty()) { + field.getAccessor().setValue(newResource, newList); + returnValue = Optional.empty(); + } else { + returnValue = Optional.of(context.getResultFactory().zip(relatedResults).doWork(relatedObjects -> { + newList.addAll(relatedObjects); + field.getAccessor().setValue(newResource, newList); + })); + } + + // Proceed to a diff to limit number of update operations + ResourceFieldAccessor oppositeIdAccessor = oppositeInformation.getIdField().getAccessor(); + List oldListIds = oldList.stream().map(oppositeIdAccessor::getValue).collect(Collectors.toList()); + List newListIds = newList.stream().map(oppositeIdAccessor::getValue).collect(Collectors.toList()); + + // Get elements in oldList but not in newList + List elementsToRemove = oldList.stream().filter(oObject -> !newListIds.contains(oppositeIdAccessor.getValue(oObject))).collect(Collectors.toList()); + // Get elements in newList but not in oldList + List elementsToAdd = newList.stream().filter(nObject -> !oldListIds.contains(oppositeIdAccessor.getValue(nObject))).collect(Collectors.toList()); + + if (oppositeField.getIdAccessor() != null) { + if (oppositeField.isCollection()) { + for (Object element : elementsToRemove) { + List ids = (List) oppositeField.getIdAccessor().getValue(element); + ids.remove(localTypeId); + oppositeField.getIdAccessor().setValue(element, ids); + oppositeEntry.getResourceRepository().update(element, oppositeQueryAdapter); + } + + for (Object element : elementsToAdd) { + List ids = (List) oppositeField.getIdAccessor().getValue(element); + ids.add(localTypeId); + oppositeEntry.getResourceRepository().update(element, oppositeQueryAdapter); + } + } else { + for (Object element : elementsToRemove) { + oppositeField.getIdAccessor().setValue(element, null); + oppositeEntry.getResourceRepository().update(element, oppositeQueryAdapter); + } + + for (Object element : elementsToAdd) { + oppositeField.getIdAccessor().setValue(element, localTypeId); + oppositeEntry.getResourceRepository().update(element, oppositeQueryAdapter); + } + } + } + return returnValue; + } + protected Optional setRelationsFieldAsync(Object newResource, RegistryEntry registryEntry, Map.Entry property, QueryAdapter queryAdapter) { Relationship relationship = property.getValue(); diff --git a/crnk-core/src/test/java/io/crnk/core/engine/internal/dispatcher/controller/ResourcePostControllerTest.java b/crnk-core/src/test/java/io/crnk/core/engine/internal/dispatcher/controller/ResourcePostControllerTest.java index 25ed1b0ed..2490079b1 100644 --- a/crnk-core/src/test/java/io/crnk/core/engine/internal/dispatcher/controller/ResourcePostControllerTest.java +++ b/crnk-core/src/test/java/io/crnk/core/engine/internal/dispatcher/controller/ResourcePostControllerTest.java @@ -14,6 +14,7 @@ import io.crnk.core.engine.document.Relationship; import io.crnk.core.engine.document.Resource; import io.crnk.core.engine.document.ResourceIdentifier; +import io.crnk.core.engine.filter.ResourceModificationFilter; import io.crnk.core.engine.filter.ResourceRelationshipModificationType; import io.crnk.core.engine.http.HttpStatus; import io.crnk.core.engine.information.resource.ResourceField; @@ -35,6 +36,7 @@ import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; +import org.mockito.Mock; import org.mockito.Mockito; public class ResourcePostControllerTest extends ControllerTestBase { @@ -433,9 +435,11 @@ public void onUpdatedLazyRelationshipDataShouldReturnThatData() { assertThat(projectsResponse.getDocument().getSingleData().get().getRelationships().get("tasks").getCollectionData().get() .get(0).getId()).isEqualTo(taskId.toString()); - Mockito.verify(modificationFilter, Mockito.times(1)) - .modifyManyRelationship(Mockito.any(), Mockito.any(ResourceField.class), - Mockito.eq(ResourceRelationshipModificationType.SET), Mockito.eq(taskIds)); + ResourceModificationFilter resourceModificationFilter = Mockito.verify(modificationFilter, Mockito.times(4)); + resourceModificationFilter.modifyAttribute(Mockito.any(), Mockito.any(ResourceField.class), + Mockito.any(), Mockito.any()); + resourceModificationFilter.modifyManyRelationship(Mockito.any(), Mockito.any(ResourceField.class), + Mockito.eq(ResourceRelationshipModificationType.SET), Mockito.eq(taskIds)); }