Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Patch, Post resource relationships on non owner field #784

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -136,6 +132,40 @@ protected RuntimeException newBodyException(String message, IOException e) {
throw new ResponseBodyException(message, e);
}

@Override
protected Optional<Result> 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<Result> setRelationsFieldAsync(Object newResource, RegistryEntry registryEntry,
Map.Entry<String, Relationship> property, QueryAdapter queryAdapter) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -246,7 +253,14 @@ protected Result<List> setRelationsAsync(Object newResource, RegistryEntry regis
ResourceFilterDirectory filterDirectory = context.getResourceFilterDirectory();
if (!checkAccess() || filterDirectory.canAccess(field, getHttpMethod(), queryAdapter.getQueryContext(), ignoreImmutableFields())) {
Optional<Result> 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,
Expand All @@ -268,6 +282,134 @@ protected Result<List> setRelationsAsync(Object newResource, RegistryEntry regis
return resultFactory.zip((List) results);
}

protected Optional<Result> 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<MetaInformation> 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<Object> previousResult = oppositeEntry.getResourceRepository().findAll(referenceQueryAdapter).map(JsonApiResponse::getEntity);

// Opposite resources that are in the relationship ids
List<Result<Object>> relatedResults = new ArrayList<>();
if (relationship.getData().isPresent()) {
LinkedList relationshipTypedIds = new LinkedList<>();
ArrayList<ResourceIdentifier> 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<Object> oldList = (DefaultResourceList) previousResult.get();

DefaultResourceList<Object> 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<Result> 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<Object> oldListIds = oldList.stream().map(oppositeIdAccessor::getValue).collect(Collectors.toList());
List<Object> newListIds = newList.stream().map(oppositeIdAccessor::getValue).collect(Collectors.toList());

// Get elements in oldList but not in newList
List<Object> elementsToRemove = oldList.stream().filter(oObject -> !newListIds.contains(oppositeIdAccessor.getValue(oObject))).collect(Collectors.toList());
// Get elements in newList but not in oldList
List<Object> 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<Object> ids = (List<Object>) oppositeField.getIdAccessor().getValue(element);
ids.remove(localTypeId);
oppositeField.getIdAccessor().setValue(element, ids);
oppositeEntry.getResourceRepository().update(element, oppositeQueryAdapter);
}

for (Object element : elementsToAdd) {
List<Object> ids = (List<Object>) 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<Result> setRelationsFieldAsync(Object newResource, RegistryEntry registryEntry,
Map.Entry<String, Relationship> property, QueryAdapter queryAdapter) {
Relationship relationship = property.getValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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));
}


Expand Down