D4CRIS-416 introduce model class to manage patch operation deserialization

This commit is contained in:
Luigi Andrea Pascarelli
2017-12-11 17:04:31 +01:00
parent 4ff9027190
commit 54288d241a
14 changed files with 500 additions and 8 deletions

View File

@@ -23,6 +23,7 @@ import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.atteo.evo.inflector.English;
import org.dspace.app.rest.converter.JsonPatchConverter;
import org.dspace.app.rest.exception.PaginationException;
import org.dspace.app.rest.exception.PatchBadRequestException;
import org.dspace.app.rest.exception.PatchUnprocessableEntityException;
@@ -35,6 +36,7 @@ import org.dspace.app.rest.model.LinkRest;
import org.dspace.app.rest.model.RestModel;
import org.dspace.app.rest.model.hateoas.DSpaceResource;
import org.dspace.app.rest.model.hateoas.EmbeddedPage;
import org.dspace.app.rest.model.patch.Patch;
import org.dspace.app.rest.model.step.UploadStatusResponse;
import org.dspace.app.rest.repository.DSpaceRestRepository;
import org.dspace.app.rest.repository.LinkRestRepository;
@@ -49,8 +51,6 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.rest.webmvc.ControllerUtils;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.data.rest.webmvc.json.patch.JsonPatchPatchConverter;
import org.springframework.data.rest.webmvc.json.patch.Patch;
import org.springframework.data.web.PagedResourcesAssembler;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.PagedResources;
@@ -263,7 +263,7 @@ public class RestResourceController implements InitializingBean {
DSpaceRestRepository<DirectlyAddressableRestModel, ID> repository = utils.getResourceRepository(apiCategory, model);
DirectlyAddressableRestModel modelObject = null;
try {
JsonPatchPatchConverter patchConverter = new JsonPatchPatchConverter(mapper);
JsonPatchConverter patchConverter = new JsonPatchConverter(mapper);
Patch patch = patchConverter.convert(jsonNode);
modelObject = repository.patch(request, apiCategory, model, id, patch);
}

View File

@@ -0,0 +1,154 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.converter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Nonnull;
import org.dspace.app.rest.model.patch.AddOperation;
import org.dspace.app.rest.model.patch.CopyOperation;
import org.dspace.app.rest.model.patch.FromOperation;
import org.dspace.app.rest.model.patch.JsonValueEvaluator;
import org.dspace.app.rest.model.patch.MoveOperation;
import org.dspace.app.rest.model.patch.Operation;
import org.dspace.app.rest.model.patch.Patch;
import org.dspace.app.rest.model.patch.RemoveOperation;
import org.dspace.app.rest.model.patch.ReplaceOperation;
import org.springframework.data.rest.webmvc.json.patch.PatchException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
*
* Convert {@link JsonNode}s containing JSON Patch to/from {@link Patch} objects.
*
* Based on {@link org.springframework.data.rest.webmvc.json.patch.JsonPatchPatchConverter}
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public class JsonPatchConverter implements PatchConverter<JsonNode> {
private final @Nonnull ObjectMapper mapper;
public JsonPatchConverter(ObjectMapper mapper) {
this.mapper = mapper;
}
/**
* Constructs a {@link Patch} object given a JsonNode.
*
* @param jsonNode a JsonNode containing the JSON Patch
* @return a {@link Patch}
*/
public Patch convert(JsonNode jsonNode) {
if (!(jsonNode instanceof ArrayNode)) {
throw new IllegalArgumentException("JsonNode must be an instance of ArrayNode");
}
ArrayNode opNodes = (ArrayNode) jsonNode;
List<Operation> ops = new ArrayList<Operation>(opNodes.size());
for (Iterator<JsonNode> elements = opNodes.elements(); elements.hasNext();) {
JsonNode opNode = elements.next();
String opType = opNode.get("op").textValue();
String path = opNode.get("path").textValue();
JsonNode valueNode = opNode.get("value");
Object value = valueFromJsonNode(path, valueNode);
String from = opNode.has("from") ? opNode.get("from").textValue() : null;
//IDEA maybe if the operation have a universal name the PatchOperation can be retrieve here not in WorkspaceItemRestRepository.evaluatePatch
if (opType.equals("replace")) {
ops.add(new ReplaceOperation(path, value));
} else if (opType.equals("remove")) {
ops.add(new RemoveOperation(path));
} else if (opType.equals("add")) {
ops.add(new AddOperation(path, value));
} else if (opType.equals("copy")) {
ops.add(new CopyOperation(path, from));
} else if (opType.equals("move")) {
ops.add(new MoveOperation(path, from));
} else {
throw new PatchException("Unrecognized operation type: " + opType);
}
}
return new Patch(ops);
}
/**
* Renders a {@link Patch} as a {@link JsonNode}.
*
* @param patch the patch
* @return a {@link JsonNode} containing JSON Patch.
*/
public JsonNode convert(Patch patch) {
List<Operation> operations = patch.getOperations();
JsonNodeFactory nodeFactory = JsonNodeFactory.instance;
ArrayNode patchNode = nodeFactory.arrayNode();
for (Operation operation : operations) {
ObjectNode opNode = nodeFactory.objectNode();
opNode.set("op", nodeFactory.textNode(operation.getOp()));
opNode.set("path", nodeFactory.textNode(operation.getPath()));
if (operation instanceof FromOperation) {
FromOperation fromOp = (FromOperation) operation;
opNode.set("from", nodeFactory.textNode(fromOp.getFrom()));
}
Object value = operation.getValue();
if (value != null) {
opNode.set("value", mapper.valueToTree(value));
}
patchNode.add(opNode);
}
return patchNode;
}
private Object valueFromJsonNode(String path, JsonNode valueNode) {
if (valueNode == null || valueNode.isNull()) {
return null;
} else if (valueNode.isTextual()) {
return valueNode.asText();
} else if (valueNode.isFloatingPointNumber()) {
return valueNode.asDouble();
} else if (valueNode.isBoolean()) {
return valueNode.asBoolean();
} else if (valueNode.isInt()) {
return valueNode.asInt();
} else if (valueNode.isLong()) {
return valueNode.asLong();
} else if (valueNode.isObject() || (valueNode.isArray())) {
return new JsonValueEvaluator(mapper, valueNode);
}
throw new PatchException(
String.format("Unrecognized valueNode type at path %s and value node %s.", path, valueNode));
}
}

View File

@@ -0,0 +1,48 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.converter;
import org.dspace.app.rest.model.patch.Patch;
import com.fasterxml.jackson.databind.JsonNode;
/**
* <p>
* A strategy interface for producing {@link Patch} instances from a patch document representation (such as JSON Patch)
* and rendering a Patch to a patch document representation. This decouples the {@link Patch} class from any specific
* patch format or library that holds the representation.
* </p>
* <p>
* For example, if the {@link Patch} is to be represented as JSON Patch, the representation type could be
* {@link JsonNode} or some other JSON library's type that holds a JSON document.
* </p>
*
* Based on {@link org.springframework.data.rest.webmvc.json.patch.PatchConverter}
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
* @param <T>
*/
public interface PatchConverter<T> {
/**
* Convert a patch document representation to a {@link Patch}.
*
* @param patchRepresentation the representation of a patch.
* @return the {@link Patch} object that the document represents.
*/
Patch convert(T patchRepresentation);
/**
* Convert a {@link Patch} to a representation object.
*
* @param patch the {@link Patch} to convert.
* @return the patch representation object.
*/
T convert(Patch patch);
}

View File

@@ -0,0 +1,21 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.patch;
/**
* Operation to track the "add" operation to the given "path".
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public class AddOperation extends Operation {
public AddOperation(String path, Object value) {
super("add", path, value);
}
}

View File

@@ -0,0 +1,22 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.patch;
/**
* Operation to track the "copy" operation to the given "path".
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public class CopyOperation extends FromOperation {
public CopyOperation(String path, String from) {
super("copy", path, from);
}
}

View File

@@ -0,0 +1,36 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.patch;
/**
* Operation to track the "from" operation to the given "path".
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public abstract class FromOperation extends Operation {
private final String from;
/**
* Constructs the operation
*
* @param op The name of the operation to perform. (e.g., 'copy')
* @param path The operation's target path. (e.g., '/foo/bar/4')
* @param from The operation's source path. (e.g., '/foo/bar/5')
*/
public FromOperation(String op, String path, String from) {
super(op, path);
this.from = from;
}
public String getFrom() {
return from;
}
}

View File

@@ -0,0 +1,50 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.patch;
import javax.annotation.Nonnull;
import org.springframework.data.rest.webmvc.json.patch.LateObjectEvaluator;
import org.springframework.data.rest.webmvc.json.patch.PatchException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link LateObjectEvaluator} implementation that assumes values represented as JSON objects.
*
* Based on {@link org.springframework.data.rest.webmvc.json.patch.JsonLateObjectEvaluator}
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public class JsonValueEvaluator implements LateObjectEvaluator {
private final @Nonnull ObjectMapper mapper;
private final @Nonnull JsonNode valueNode;
public JsonValueEvaluator(ObjectMapper mapper, JsonNode valueNode) {
this.mapper = mapper;
this.valueNode = valueNode;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.json.patch.LateObjectEvaluator#evaluate(java.lang.Class)
*/
@Override
public <T> Object evaluate(Class<T> type) {
try {
return mapper.readValue(valueNode.traverse(), type);
} catch (Exception e) {
throw new PatchException(String.format("Could not read %s into %s!", valueNode, type), e);
}
}
}

View File

@@ -0,0 +1,22 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.patch;
/**
* Operation to track the "move" operation to the given "path".
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public class MoveOperation extends FromOperation {
public MoveOperation(String path, String from) {
super("move", path, from);
}
}

View File

@@ -0,0 +1,49 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.patch;
/**
*
* Abstract base class representing and providing support methods for patch operations.
*
* Based on {@link org.springframework.data.rest.webmvc.json.patch.PatchOperation}
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public abstract class Operation {
protected String op;
protected String path;
protected Object value;
public Operation(String operation, String path) {
this.op = operation;
this.path = path;
this.value = null;
}
public Operation(String operation, String path, Object value) {
this.op = operation;
this.path = path;
this.value = value;
}
public String getOp() {
return op;
}
public String getPath() {
return path;
}
public Object getValue() {
return value;
}
}

View File

@@ -0,0 +1,46 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.patch;
import java.util.List;
/**
* <p>
* Represents a Patch.
* </p>
* <p>
* This class (and {@link Operation} capture the definition of a patch, but are not coupled to any specific patch
* representation.
* </p>
*
* Based on {@link org.springframework.data.rest.webmvc.json.patch.Patch}
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public class Patch {
private final List<Operation> operations;
public Patch(List<Operation> operations) {
this.operations = operations;
}
/**
* @return the number of operations that make up this patch.
*/
public int size() {
return operations.size();
}
public List<Operation> getOperations() {
return operations;
}
}

View File

@@ -0,0 +1,22 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.patch;
/**
* Operation to track the "remove" operation to the given "path".
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public class RemoveOperation extends Operation {
public RemoveOperation(String path) {
super("remove", path);
}
}

View File

@@ -0,0 +1,22 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.patch;
/**
* Operation to track the "replace" operation to the given "path".
*
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public class ReplaceOperation extends Operation {
public ReplaceOperation(String path, Object value) {
super("replace", path, value);
}
}

View File

@@ -17,6 +17,7 @@ import org.dspace.app.rest.exception.PatchBadRequestException;
import org.dspace.app.rest.exception.PatchUnprocessableEntityException;
import org.dspace.app.rest.model.DirectlyAddressableRestModel;
import org.dspace.app.rest.model.hateoas.DSpaceResource;
import org.dspace.app.rest.model.patch.Patch;
import org.dspace.app.rest.model.step.UploadStatusResponse;
import org.dspace.app.util.DCInputsReaderException;
import org.dspace.authorize.AuthorizeException;
@@ -25,7 +26,6 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.webmvc.json.patch.Patch;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.multipart.MultipartFile;

View File

@@ -21,6 +21,8 @@ import org.dspace.app.rest.converter.WorkspaceItemConverter;
import org.dspace.app.rest.exception.PatchBadRequestException;
import org.dspace.app.rest.model.WorkspaceItemRest;
import org.dspace.app.rest.model.hateoas.WorkspaceItemResource;
import org.dspace.app.rest.model.patch.Operation;
import org.dspace.app.rest.model.patch.Patch;
import org.dspace.app.rest.model.step.UploadBitstreamRest;
import org.dspace.app.rest.submit.AbstractRestProcessingStep;
import org.dspace.app.rest.submit.SubmissionService;
@@ -49,8 +51,6 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.webmvc.json.patch.Patch;
import org.springframework.data.rest.webmvc.json.patch.PatchOperation;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@@ -242,10 +242,10 @@ public class WorkspaceItemRestRepository extends DSpaceRestRepository<WorkspaceI
@Override
public void patch(Context context, HttpServletRequest request, String apiCategory, String model, Integer id, Patch patch) throws SQLException, AuthorizeException {
List<PatchOperation> operations = patch.getOperations();
List<Operation> operations = patch.getOperations();
WorkspaceItemRest wsi = findOne(id);
WorkspaceItem source = wis.find(context, id);
for(PatchOperation op : operations) {
for(Operation op : operations) {
//the value in the position 0 is a null value
String[] path = op.getPath().substring(1).split("/",3);
if(OPERATION_PATH_SECTIONS.equals(path[0])) {