diff --git a/spring-data-rest-tests/spring-data-rest-tests-mongodb/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java b/spring-data-rest-tests/spring-data-rest-tests-mongodb/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java index b8cd4ba4c..eb5fecb21 100644 --- a/spring-data-rest-tests/spring-data-rest-tests-mongodb/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java +++ b/spring-data-rest-tests/spring-data-rest-tests-mongodb/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java @@ -20,6 +20,7 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.springframework.data.rest.tests.mongodb.TestUtils.*; +import java.util.ArrayList; import java.util.Arrays; import org.junit.Before; @@ -119,7 +120,7 @@ public class JsonPatchHandlerUnitTests { User christoph = new User(); christoph.firstname = "Christoph"; - this.user.colleagues = Arrays.asList(thomas, christoph); + this.user.colleagues = new ArrayList(Arrays.asList(thomas, christoph)); String input = "[{ \"op\": \"remove\", \"path\": \"/colleagues/0\" }]"; diff --git a/spring-data-rest-webmvc/pom.xml b/spring-data-rest-webmvc/pom.xml index ea5cd3f05..be6cbc57b 100644 --- a/spring-data-rest-webmvc/pom.xml +++ b/spring-data-rest-webmvc/pom.xml @@ -79,14 +79,6 @@ ${jackson} true - - - - - com.github.fge - json-patch - 1.7 - diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/JsonPatchHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/JsonPatchHandler.java index 8b847eea8..fe222bb3f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/JsonPatchHandler.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/JsonPatchHandler.java @@ -16,26 +16,18 @@ package org.springframework.data.rest.webmvc.config; import java.io.InputStream; -import java.lang.reflect.Field; -import java.util.List; import org.springframework.data.rest.webmvc.IncomingRequest; import org.springframework.data.rest.webmvc.RestMediaTypes; import org.springframework.data.rest.webmvc.json.DomainObjectReader; +import org.springframework.data.rest.webmvc.json.patch.JsonPatchPatchConverter; +import org.springframework.data.rest.webmvc.json.patch.Patch; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.type.CollectionType; -import com.github.fge.jackson.jsonpointer.JsonPointer; -import com.github.fge.jsonpatch.JsonPatchOperation; -import com.github.fge.jsonpatch.RemoveOperation; -import com.github.fge.jsonpatch.ReplaceOperation; /** * Component to apply JSON Patch and JSON Merge Patch payloads to existing domain objects. The implementation uses the @@ -49,16 +41,6 @@ import com.github.fge.jsonpatch.ReplaceOperation; */ class JsonPatchHandler { - private static final Field PATH_FIELD; - - static { - - Field field = ReflectionUtils.findField(JsonPatchOperation.class, "path"); - ReflectionUtils.makeAccessible(field); - - PATH_FIELD = field; - } - private final ObjectMapper mapper; private final ObjectMapper sourceMapper; private final DomainObjectReader reader; @@ -104,26 +86,7 @@ class JsonPatchHandler { T applyPatch(InputStream source, T target) throws Exception { - List readValue = getPatchOperations(source); - - JsonNode existingAsNode = mapper.readTree(sourceMapper.writeValueAsBytes(target)); - JsonNode patchedNode = existingAsNode; - - for (JsonPatchOperation operation : readValue) { - - if (operation instanceof RemoveOperation) { - - // Replace remove operation with replace operation and a value of null. - JsonPointer path = (JsonPointer) ReflectionUtils.getField(PATH_FIELD, operation); - patchedNode = isCollectionElementReference(path) ? operation.apply(patchedNode) - : new ReplaceOperation(path, NullNode.getInstance()).apply(patchedNode); - - } else { - patchedNode = operation.apply(patchedNode); - } - } - - return reader.merge((ObjectNode) patchedNode, target, mapper); + return getPatchOperations(source).apply(target, (Class) target.getClass()); } T applyMergePatch(InputStream source, T existingObject) throws Exception { @@ -141,40 +104,13 @@ class JsonPatchHandler { * @return * @throws HttpMessageNotReadableException in case the payload can't be read. */ - private List getPatchOperations(InputStream source) { - - CollectionType listOfOperationsType = mapper.getTypeFactory().constructCollectionType(List.class, - JsonPatchOperation.class); + private Patch getPatchOperations(InputStream source) { try { - return mapper.readValue(source, listOfOperationsType); + return new JsonPatchPatchConverter().convert(mapper.readTree(source)); } catch (Exception o_O) { throw new HttpMessageNotReadableException( String.format("Could not read PATCH operations! Expected %s!", RestMediaTypes.JSON_PATCH_JSON), o_O); } } - - /** - * Returns whether the trailing element of the given {@link JsonPointer} is a pointer into an array or collection. - * - * @param pointer must not be {@literal null}. - * @return - */ - private static boolean isCollectionElementReference(JsonPointer pointer) { - - String[] segments = pointer.toString().split("/"); - - if (segments.length == 0) { - return false; - } - - String trailing = segments[segments.length - 1]; - - try { - Integer.parseInt(trailing); - return true; - } catch (NumberFormatException o_O) { - return false; - } - } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/AddOperation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/AddOperation.java new file mode 100644 index 000000000..ca8fbf981 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/AddOperation.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +/** + * Operation to add a new value to the given "path". + * Will throw a {@link PatchException} if the path is invalid or if the given value + * is not assignable to the given path. + * + * @author Craig Walls + */ +public class AddOperation extends PatchOperation { + + /** + * Constructs the add operation + * @param path The path where the value will be added. (e.g., '/foo/bar/4') + * @param value The value to add. + */ + public AddOperation(String path, Object value) { + super("add", path, value); + } + + @Override + void perform(Object targetObject, Class type) { + addValue(targetObject, evaluateValueFromTarget(targetObject, type)); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/CopyOperation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/CopyOperation.java new file mode 100644 index 000000000..e2ba689ef --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/CopyOperation.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import static org.springframework.data.rest.webmvc.json.patch.PathToSpEL.*; + +/** + *

+ * Operation to copy a value from the given "from" path to the given "path". Will throw a {@link PatchException} if + * either path is invalid or if the object at the from path is not assignable to the given path. + *

+ *

+ * NOTE: When dealing with lists, the copy operation may yield undesirable results. If a list is produced from a + * database query, it's likely that the list contains items with a unique ID. Copying an item in the list will result + * with a list that has a duplicate object, with the same ID. The best case post-patch scenario is that each copy of the + * item will be saved, unchanged, to the database and later queries for the list will not include the duplicate. The + * worst case post-patch scenario is that a following operation changes some properties of the copy, but does not change + * the ID. When saved, both the original and the copy will be saved, but the last one saved will overwrite the first. + * Effectively only one copy will survive post-save. + *

+ *

+ * In light of this, it's probably a good idea to perform a "replace" after a "copy" to set the ID property (which may + * or may not be "id"). + *

+ * + * @author Craig Walls + */ +public class CopyOperation extends FromOperation { + + /** + * Constructs the copy operation + * + * @param path The path to copy the source value to. (e.g., '/foo/bar/4') + * @param from The source path from which a value will be copied. (e.g., '/foo/bar/5') + */ + public CopyOperation(String path, String from) { + super("copy", path, from); + } + + @Override + void perform(Object target, Class type) { + addValue(target, pathToExpression(from).getValue(target)); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/FromOperation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/FromOperation.java new file mode 100644 index 000000000..05a3e9e58 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/FromOperation.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +/** + * Abstract base class for operations requiring a source property, such as "copy" and "move". + * (e.g., copy from here to there. + * @author Craig Walls + */ +public abstract class FromOperation extends PatchOperation { + + protected 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; + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/JsonLateObjectEvaluator.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/JsonLateObjectEvaluator.java new file mode 100644 index 000000000..55de8965e --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/JsonLateObjectEvaluator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * {@link LateObjectEvaluator} implementation that assumes values represented as JSON objects. + * + * @author Craig Walls + */ +class JsonLateObjectEvaluator implements LateObjectEvaluator { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private JsonNode valueNode; + + public JsonLateObjectEvaluator(JsonNode valueNode) { + this.valueNode = valueNode; + } + + @Override + public Object evaluate(Class type) { + try { + return MAPPER.readValue(valueNode.traverse(), type); + } catch (Exception e) { + return null; + } + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/JsonPatchPatchConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/JsonPatchPatchConverter.java new file mode 100644 index 000000000..d56bf9789 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/JsonPatchPatchConverter.java @@ -0,0 +1,156 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.fasterxml.jackson.core.JsonPointer; +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. + * + * @author Craig Walls + */ +public class JsonPatchPatchConverter implements PatchConverter { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * 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 ops = new ArrayList(opNodes.size()); + for (Iterator 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; + + if (opType.equals("test")) { + ops.add(new TestOperation(path, value)); + } else 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 operations = patch.getOperations(); + JsonNodeFactory nodeFactory = JsonNodeFactory.instance; + ArrayNode patchNode = nodeFactory.arrayNode(); + for (PatchOperation 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()) { + return new JsonLateObjectEvaluator(valueNode); + } else if (valueNode.isArray()) { + // TODO: Convert valueNode to array + } + + return null; + } + + /** + * Returns whether the trailing element of the given {@link JsonPointer} is a pointer into an array or collection. + * + * @param pointer must not be {@literal null}. + * @return + */ + private static boolean isCollectionElementReference(String pointer) { + + String[] segments = pointer.split("/"); + + if (segments.length == 0) { + return false; + } + + String trailing = segments[segments.length - 1]; + + try { + Integer.parseInt(trailing); + return true; + } catch (NumberFormatException o_O) { + return false; + } + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/LateObjectEvaluator.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/LateObjectEvaluator.java new file mode 100644 index 000000000..aa4303cbd --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/LateObjectEvaluator.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +/** + *

+ * Strategy interface for resolving values from an operation definition. + *

+ * + *

+ * {@link Patch} implementation generically defines a patch without being tied to any particular + * patch specification. But it's important to know the patch format when resolving the value of + * an operation, as the value format will likely be tied to the patch specification. For example, + * the value attribute of a JSON Patch operation will contain a JSON object. A different + * patch specification may define values in some non-JSON format. + *

+ * + *

+ * This interface allows for pluggable evaluation of values, allowing {@link Patch} to remain + * independent of any specific patch representation. + *

+ * + * @author Craig Walls + */ +public interface LateObjectEvaluator { + + Object evaluate(Class type); + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/MoveOperation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/MoveOperation.java new file mode 100644 index 000000000..863c852e8 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/MoveOperation.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +/** + *

+ * Operation that moves a value from the given "from" path to the given "path". + * Will throw a {@link PatchException} if either path is invalid or if the from path is non-nullable. + *

+ * + *

+ * NOTE: When dealing with lists, the move operation may effectively be a no-op. + * That's because the order of a list is probably dictated by a database query that produced the list. + * Moving things around in the list will have no bearing on the values of each item in the list. + * When the same list resource is retrieved again later, the order will again be decided by the query, + * effectively undoing any previous move operation. + *

+ * + * @author Craig Walls + */ +public class MoveOperation extends FromOperation { + + /** + * Constructs the move operation. + * @param path The path to move the source value to. (e.g., '/foo/bar/4') + * @param from The source path from which a value will be moved. (e.g., '/foo/bar/5') + */ + public MoveOperation(String path, String from) { + super("move", path, from); + } + + @Override + void perform(Object target, Class type) { + addValue(target, popValueAtPath(target, from)); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/Patch.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/Patch.java new file mode 100644 index 000000000..05f25ecea --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/Patch.java @@ -0,0 +1,87 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import java.util.List; + +/** + *

+ * Represents a Patch. + *

+ *

+ * This class (and {@link PatchOperation} capture the definition of a patch, but are not coupled to any specific patch + * representation. + *

+ * + * @author Craig Walls + */ +public class Patch { + + private final List operations; + + public Patch(List operations) { + this.operations = operations; + } + + /** + * @return the number of operations that make up this patch. + */ + public int size() { + return operations.size(); + } + + public List getOperations() { + return operations; + } + + /** + * Applies the Patch to a given Object graph. Makes a copy of the given object so that it will remain unchanged after + * application of the patch and in case any errors occur while performing the patch. + * + * @param in The object graph to apply the patch to. + * @param type The object type. + * @param the object type. + * @return An object graph modified by the patch. + * @throws PatchException if there are any errors while applying the patch. + */ + public T apply(T in, Class type) throws PatchException { + + for (PatchOperation operation : operations) { + operation.perform(in, type); + } + + return in; + } + + /** + * Applies the Patch to a given List of objects. Makes a copy of the given list so that it will remain unchanged after + * application of the patch and in case any errors occur while performing the patch. + * + * @param in The list to apply the patch to. + * @param type The list's generic type. + * @param the list's generic type. + * @return An list modified by the patch. + * @throws PatchException if there are any errors while applying the patch. + */ + public List apply(List in, Class type) throws PatchException { + + for (PatchOperation operation : operations) { + operation.perform(in, type); + } + + return in; + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchConverter.java new file mode 100644 index 000000000..0f00404c4 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchConverter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + *

+ * 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. + *

+ *

+ * 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. + *

+ * + * @author Craig Walls + * @param A type holding a representation of the patch. For example, a JsonNode if working with JSON Patch. + */ +public interface PatchConverter { + + /** + * 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); + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchException.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchException.java new file mode 100644 index 000000000..5eb393caf --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +/** + * Exception thrown if an error occurs in the course of applying a Patch. + * + * @author Craig Walls + */ +public class PatchException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public PatchException(String message) { + super(message); + } + + public PatchException(String message, Exception e) { + super(message, e); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchOperation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchOperation.java new file mode 100644 index 000000000..774d1e769 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchOperation.java @@ -0,0 +1,195 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import static org.springframework.data.rest.webmvc.json.patch.PathToSpEL.*; + +import java.util.List; + +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionException; + +/** + * Abstract base class representing and providing support methods for patch operations. + * + * @author Craig Walls + */ +public abstract class PatchOperation { + + protected final String op; + + protected final String path; + + protected final Object value; + + protected final Expression spelExpression; + + /** + * Constructs the operation. + * + * @param op the operation name. (e.g., 'move') + * @param path the path to perform the operation on. (e.g., '/1/description') + */ + public PatchOperation(String op, String path) { + this(op, path, null); + } + + /** + * Constructs the operation. + * + * @param op the operation name. (e.g., 'move') + * @param path the path to perform the operation on. (e.g., '/1/description') + * @param value the value to apply in the operation. Could be an actual value or an implementation of + * {@link LateObjectEvaluator}. + */ + public PatchOperation(String op, String path, Object value) { + this.op = op; + this.path = path; + this.value = value; + this.spelExpression = pathToExpression(path); + } + + /** + * @return the operation name + */ + public String getOp() { + return op; + } + + /** + * @return the operation path + */ + public String getPath() { + return path; + } + + /** + * @return the operation's value (or {@link LateObjectEvaluator}) + */ + public Object getValue() { + return value; + } + + /** + * Pops a value from the given path. + * + * @param target the target from which to pop a value. + * @param removePath the path from which to pop a value. Must be a list. + * @return the value popped from the list + */ + protected Object popValueAtPath(Object target, String removePath) { + Integer listIndex = targetListIndex(removePath); + Expression expression = pathToExpression(removePath); + Object value = expression.getValue(target); + if (listIndex == null) { + try { + expression.setValue(target, null); + return value; + } catch (NullPointerException e) { + throw new PatchException("Path '" + removePath + "' is not nullable."); + } + } else { + Expression parentExpression = pathToParentExpression(removePath); + List list = (List) parentExpression.getValue(target); + list.remove(listIndex >= 0 ? listIndex.intValue() : list.size() - 1); + return value; + } + } + + /** + * Adds a value to the operation's path. If the path references a list index, the value is added to the list at the + * given index. If the path references an object property, the property is set to the value. + * + * @param target The target object. + * @param value The value to add. + */ + protected void addValue(Object target, Object value) { + Expression parentExpression = pathToParentExpression(path); + Object parent = parentExpression != null ? parentExpression.getValue(target) : null; + Integer listIndex = targetListIndex(path); + if (parent == null || !(parent instanceof List) || listIndex == null) { + spelExpression.setValue(target, value); + } else { + @SuppressWarnings("unchecked") + List list = (List) parentExpression.getValue(target); + int addAtIndex = listIndex >= 0 ? listIndex.intValue() : list.size(); + list.add(addAtIndex, value); + } + } + + /** + * Sets a value to the operation's path. + * + * @param target The target object. + * @param value The value to set. + */ + protected void setValueOnTarget(Object target, Object value) { + spelExpression.setValue(target, value); + } + + /** + * Retrieves a value from the operation's path. + * + * @param target the target object. + * @return the value at the path on the given target object. + */ + protected Object getValueFromTarget(Object target) { + try { + return spelExpression.getValue(target); + } catch (ExpressionException e) { + throw new PatchException("Unable to get value from target", e); + } + } + + /** + * Performs late-value evaluation on the operation value if the value is a {@link LateObjectEvaluator}. + * + * @param targetObject the target object, used as assistance in determining the evaluated object's type. + * @param entityType the entityType + * @param the entity type + * @return the result of late-value evaluation if the value is a {@link LateObjectEvaluator}; the value itself + * otherwise. + */ + protected Object evaluateValueFromTarget(Object targetObject, Class entityType) { + return value instanceof LateObjectEvaluator ? ((LateObjectEvaluator) value).evaluate(entityType) : value; + } + + /** + * Perform the operation. + * + * @param target the target of the operation. + */ + abstract void perform(Object target, Class type); + + // private helpers + + private Integer targetListIndex(String path) { + String[] pathNodes = path.split("\\/"); + + String lastNode = pathNodes[pathNodes.length - 1]; + + if ("~".equals(lastNode)) { + return -1; + } + + try { + return Integer.parseInt(lastNode); + } catch (NumberFormatException e) { + return null; + } + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PathToSpEL.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PathToSpEL.java new file mode 100644 index 000000000..f5412045f --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PathToSpEL.java @@ -0,0 +1,106 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import java.util.Arrays; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Utilities for converting patch paths to/from SpEL expressions. For example, "/foo/bars/1/baz" becomes + * "foo.bars[1].baz". + * + * @author Craig Walls + * @author Oliver Gierke + */ +public class PathToSpEL { + + private static final SpelExpressionParser SPEL_EXPRESSION_PARSER = new SpelExpressionParser(); + + /** + * Converts a patch path to an {@link Expression}. + * + * @param path the patch path to convert. + * @return an {@link Expression} + */ + public static Expression pathToExpression(String path) { + return SPEL_EXPRESSION_PARSER.parseExpression(pathToSpEL(path)); + } + + /** + * Convenience method to convert a SpEL String to an {@link Expression}. + * + * @param spel the SpEL expression as a String + * @return an {@link Expression} + */ + public static Expression spelToExpression(String spel) { + return SPEL_EXPRESSION_PARSER.parseExpression(spel); + } + + /** + * Produces an expression targeting the parent of the object that the given path targets. + * + * @param path the path to find a parent expression for. + * @return an {@link Expression} targeting the parent of the object specifed by path. + */ + public static Expression pathToParentExpression(String path) { + return spelToExpression(pathNodesToSpEL(copyOf(path.split("\\/"), path.split("\\/").length - 1))); + } + + // private helpers + + private static String pathToSpEL(String path) { + return pathNodesToSpEL(path.split("\\/")); + } + + private static String pathNodesToSpEL(String[] pathNodes) { + StringBuilder spelBuilder = new StringBuilder(); + + for (int i = 0; i < pathNodes.length; i++) { + String pathNode = pathNodes[i]; + if (pathNode.length() == 0) { + continue; + } + + if ("~".equals(pathNode)) { + spelBuilder.append("[size() - 1]"); + continue; + } + + try { + int index = Integer.parseInt(pathNode); + spelBuilder.append('[').append(index).append(']'); + } catch (NumberFormatException e) { + if (spelBuilder.length() > 0) { + spelBuilder.append('.'); + } + spelBuilder.append(pathNode); + } + } + + String spel = spelBuilder.toString(); + if (spel.length() == 0) { + spel = "#this"; + } + return spel; + } + + @SuppressWarnings("unchecked") + private static T[] copyOf(T[] original, int newLength) { + return (T[]) Arrays.copyOf(original, newLength, original.getClass()); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/RemoveOperation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/RemoveOperation.java new file mode 100644 index 000000000..f9a71642e --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/RemoveOperation.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +/** + * Operation that removes the value at the given path. + * Will throw a {@link PatchException} if the given path isn't valid or if the path is non-nullable. + * + * @author Craig Walls + */ +public class RemoveOperation extends PatchOperation { + + /** + * Constructs the remove operation + * @param path The path of the value to be removed. (e.g., '/foo/bar/4') + */ + public RemoveOperation(String path) { + super("remove", path); + } + + @Override + void perform(Object target, Class type) { + popValueAtPath(target, path); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/ReplaceOperation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/ReplaceOperation.java new file mode 100644 index 000000000..e287168b2 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/ReplaceOperation.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +/** + * Operation that replaces the value at the given path with a new value. + * + * @author Craig Walls + */ +public class ReplaceOperation extends PatchOperation { + + /** + * Constructs the replace operation + * @param path The path whose value is to be replaced. (e.g., '/foo/bar/4') + * @param value The value that will replace the current path value. + */ + public ReplaceOperation(String path, Object value) { + super("replace", path, value); + } + + @Override + void perform(Object target, Class type) { + setValueOnTarget(target, evaluateValueFromTarget(target, type)); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/TestOperation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/TestOperation.java new file mode 100644 index 000000000..27df04aa0 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/TestOperation.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.util.ObjectUtils; + +/** + *

+ * Operation to test values on a given target. + *

+ *

+ * If the value given matches the value given at the path, the operation completes as a no-op. On the other hand, if the + * values do not match or if there are any errors interpreting the path, a {@link PatchException} will be thrown. + *

+ * + * @author Craig Walls + */ +class TestOperation extends PatchOperation { + + /** + * Constructs the test operation + * + * @param path The path to test. (e.g., '/foo/bar/4') + * @param value The value to test the path against. + */ + public TestOperation(String path, Object value) { + super("test", path, value); + } + + @Override + void perform(Object target, Class type) { + Object expected = normalizeIfNumber(evaluateValueFromTarget(target, type)); + Object actual = normalizeIfNumber(getValueFromTarget(target)); + if (!ObjectUtils.nullSafeEquals(expected, actual)) { + throw new PatchException("Test against path '" + path + "' failed."); + } + } + + private Object normalizeIfNumber(Object expected) { + if (expected instanceof Double || expected instanceof Float) { + expected = BigDecimal.valueOf(((Number) expected).doubleValue()); + } else if (expected instanceof Number) { + expected = BigInteger.valueOf(((Number) expected).longValue()); + } + return expected; + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/AddOperationTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/AddOperationTest.java new file mode 100644 index 000000000..677318860 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/AddOperationTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class AddOperationTest { + + + @Test + public void addBooleanPropertyValue() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + AddOperation add = new AddOperation("/1/complete", true); + add.perform(todos, Todo.class); + + assertTrue(todos.get(1).isComplete()); + } + + @Test + public void addStringPropertyValue() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + AddOperation add = new AddOperation("/1/description", "BBB"); + add.perform(todos, Todo.class); + + assertEquals("BBB", todos.get(1).getDescription()); + } + + + @Test + public void addItemToList() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + AddOperation add = new AddOperation("/1", new Todo(null, "D", true)); + add.perform(todos, Todo.class); + + assertEquals(4, todos.size()); + assertEquals("A", todos.get(0).getDescription()); + assertFalse(todos.get(0).isComplete()); + assertEquals("D", todos.get(1).getDescription()); + assertTrue(todos.get(1).isComplete()); + assertEquals("B", todos.get(2).getDescription()); + assertFalse(todos.get(2).isComplete()); + assertEquals("C", todos.get(3).getDescription()); + assertFalse(todos.get(3).isComplete()); + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/CopyOperationTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/CopyOperationTest.java new file mode 100644 index 000000000..f309f5524 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/CopyOperationTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class CopyOperationTest { + + @Test + public void copyBooleanPropertyValue() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + CopyOperation copy = new CopyOperation("/1/complete", "/0/complete"); + copy.perform(todos, Todo.class); + + assertTrue(todos.get(1).isComplete()); + } + + @Test + public void copyStringPropertyValue() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + CopyOperation copy = new CopyOperation("/1/description", "/0/description"); + copy.perform(todos, Todo.class); + + assertEquals("A", todos.get(1).getDescription()); + } + + @Test + public void copyBooleanPropertyValueIntoStringProperty() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + CopyOperation copy = new CopyOperation("/1/description", "/0/complete"); + copy.perform(todos, Todo.class); + + assertEquals("true", todos.get(1).getDescription()); + } + + @Test + public void copyListElementToBeginningOfList() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", true)); + todos.add(new Todo(3L, "C", false)); + + CopyOperation copy = new CopyOperation("/0", "/1"); + copy.perform(todos, Todo.class); + + assertEquals(4, todos.size()); + assertEquals(2L, todos.get(0).getId().longValue()); // NOTE: This could be problematic if you try to save it to a DB because there'll be duplicate IDs + assertEquals("B", todos.get(0).getDescription()); + assertTrue(todos.get(0).isComplete()); + } + + @Test + public void copyListElementToMiddleOfList() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + CopyOperation copy = new CopyOperation("/2", "/0"); + copy.perform(todos, Todo.class); + + assertEquals(4, todos.size()); + assertEquals(1L, todos.get(2).getId().longValue()); // NOTE: This could be problematic if you try to save it to a DB because there'll be duplicate IDs + assertEquals("A", todos.get(2).getDescription()); + assertTrue(todos.get(2).isComplete()); + } + + @Test + public void copyListElementToEndOfList_usingIndex() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + CopyOperation copy = new CopyOperation("/3", "/0"); + copy.perform(todos, Todo.class); + + assertEquals(4, todos.size()); + assertEquals(1L, todos.get(3).getId().longValue()); // NOTE: This could be problematic if you try to save it to a DB because there'll be duplicate IDs + assertEquals("A", todos.get(3).getDescription()); + assertTrue(todos.get(3).isComplete()); + } + + @Test + public void copyListElementToEndOfList_usingTilde() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + CopyOperation copy = new CopyOperation("/~", "/0"); + copy.perform(todos, Todo.class); + + assertEquals(4, todos.size()); + assertEquals(new Todo(1L, "A", true), todos.get(3)); // NOTE: This could be problematic if you try to save it to a DB because there'll be duplicate IDs + } + + @Test + public void copyListElementFromEndOfList_usingTilde() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + CopyOperation copy = new CopyOperation("/0", "/~"); + copy.perform(todos, Todo.class); + + assertEquals(4, todos.size()); + assertEquals(new Todo(3L, "C", false), todos.get(0)); // NOTE: This could be problematic if you try to save it to a DB because there'll be duplicate IDs + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/JsonPatchTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/JsonPatchTest.java new file mode 100644 index 000000000..25af34a18 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/JsonPatchTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class JsonPatchTest { + + @Test + public void manySuccessfulOperations() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + todos.add(new Todo(4L, "D", false)); + todos.add(new Todo(5L, "E", false)); + todos.add(new Todo(6L, "F", false)); + + Patch patch = readJsonPatch("patch-many-successful-operations.json"); + assertEquals(6, patch.size()); + + List patchedTodos = patch.apply(todos, Todo.class); + + assertEquals(6, todos.size()); + assertTrue(patchedTodos.get(1).isComplete()); + assertEquals("C", patchedTodos.get(3).getDescription()); + assertEquals("A", patchedTodos.get(4).getDescription()); + } + + @Test + public void failureAtBeginning() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + todos.add(new Todo(4L, "D", false)); + todos.add(new Todo(5L, "E", false)); + todos.add(new Todo(6L, "F", false)); + + Patch patch = readJsonPatch("patch-failing-operation-first.json"); + + try { + patch.apply(todos, Todo.class); + fail(); + } catch (PatchException e) { + assertEquals("Test against path '/5/description' failed.", e.getMessage()); + } + + // nothing should have changed + assertEquals(6, todos.size()); + assertFalse(todos.get(1).isComplete()); + assertEquals("D", todos.get(3).getDescription()); + assertEquals("E", todos.get(4).getDescription()); + assertEquals("F", todos.get(5).getDescription()); + } + + @Test + public void failureInMiddle() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + todos.add(new Todo(4L, "D", false)); + todos.add(new Todo(5L, "E", false)); + todos.add(new Todo(6L, "F", false)); + + Patch patch = readJsonPatch("patch-failing-operation-in-middle.json"); + + try { + patch.apply(todos, Todo.class); + fail(); + } catch (PatchException e) { + assertEquals("Test against path '/5/description' failed.", e.getMessage()); + } + + // nothing should have changed + assertEquals(6, todos.size()); + assertFalse(todos.get(1).isComplete()); + assertEquals("D", todos.get(3).getDescription()); + assertEquals("E", todos.get(4).getDescription()); + assertEquals("F", todos.get(5).getDescription()); + } + + private Patch readJsonPatch(String jsonPatchFile) throws IOException, JsonParseException, JsonMappingException { + ClassPathResource resource = new ClassPathResource(jsonPatchFile, getClass()); + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readValue(resource.getInputStream(), JsonNode.class); + Patch patch = new JsonPatchPatchConverter().convert(node); + return patch; + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/MoveOperationTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/MoveOperationTest.java new file mode 100644 index 000000000..5c3178efa --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/MoveOperationTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class MoveOperationTest { + + @Test + public void moveBooleanPropertyValue() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + try { + MoveOperation move = new MoveOperation("/1/complete", "/0/complete"); + move.perform(todos, Todo.class); + fail(); + } catch (PatchException e) { + assertEquals("Path '/0/complete' is not nullable.", e.getMessage()); + } + assertFalse(todos.get(1).isComplete()); + + } + + @Test + public void moveStringPropertyValue() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + MoveOperation move = new MoveOperation("/1/description", "/0/description"); + move.perform(todos, Todo.class); + + assertEquals("A", todos.get(1).getDescription()); + } + + @Test + public void moveBooleanPropertyValueIntoStringProperty() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + try { + MoveOperation move = new MoveOperation("/1/description", "/0/complete"); + move.perform(todos, Todo.class); + fail(); + } catch (PatchException e) { + assertEquals("Path '/0/complete' is not nullable.", e.getMessage()); + } + assertEquals("B", todos.get(1).getDescription()); + } + + // + // NOTE: Moving an item about in a list probably has zero effect, as the order of the list is + // usually determined by the DB query that produced the list. Moving things around in a + // java.util.List and then saving those items really means nothing to the DB, as the + // properties that determined the original order are still the same and will result in + // the same order when the objects are queries again. + // + + @Test + public void moveListElementToBeginningOfList() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", true)); + todos.add(new Todo(3L, "C", false)); + + MoveOperation move = new MoveOperation("/0", "/1"); + move.perform(todos, Todo.class); + + assertEquals(3, todos.size()); + assertEquals(2L, todos.get(0).getId().longValue()); + assertEquals("B", todos.get(0).getDescription()); + assertTrue(todos.get(0).isComplete()); + } + + @Test + public void moveListElementToMiddleOfList() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + MoveOperation move = new MoveOperation("/2", "/0"); + move.perform(todos, Todo.class); + + assertEquals(3, todos.size()); + assertEquals(1L, todos.get(2).getId().longValue()); + assertEquals("A", todos.get(2).getDescription()); + assertTrue(todos.get(2).isComplete()); + } + + @Test + public void moveListElementToEndOfList_usingIndex() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + MoveOperation move = new MoveOperation("/2", "/0"); + move.perform(todos, Todo.class); + + assertEquals(3, todos.size()); + assertEquals(1L, todos.get(2).getId().longValue()); + assertEquals("A", todos.get(2).getDescription()); + assertTrue(todos.get(2).isComplete()); + } + + @Test + public void moveListElementToBeginningOfList_usingTilde() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(3L, "C", false)); + todos.add(new Todo(4L, "E", false)); + todos.add(new Todo(2L, "G", false)); + + List expected = new ArrayList(); + expected.add(new Todo(1L, "A", true)); + expected.add(new Todo(2L, "G", false)); + expected.add(new Todo(3L, "C", false)); + expected.add(new Todo(4L, "E", false)); + + MoveOperation move = new MoveOperation("/1", "/~"); + move.perform(todos, Todo.class); + assertEquals(expected, todos); + } + + @Test + public void moveListElementToEndOfList_usingTilde() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", true)); + todos.add(new Todo(2L, "G", false)); + todos.add(new Todo(3L, "C", false)); + todos.add(new Todo(4L, "E", false)); + + List expected = new ArrayList(); + expected.add(new Todo(1L, "A", true)); + expected.add(new Todo(3L, "C", false)); + expected.add(new Todo(4L, "E", false)); + expected.add(new Todo(2L, "G", false)); + + MoveOperation move = new MoveOperation("/~", "/1"); + move.perform(todos, Todo.class); + assertEquals(expected, todos); + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/PathToSpelTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/PathToSpelTest.java new file mode 100644 index 000000000..db6e7f6c2 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/PathToSpelTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.springframework.expression.Expression; + +public class PathToSpelTest { + + @Test + public void listIndex() { + Expression expr = PathToSpEL.pathToExpression("/1/description"); + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + assertEquals("B", (String) expr.getValue(todos)); + } + + @Test + public void listTilde() { + Expression expr = PathToSpEL.pathToExpression("/~/description"); + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + assertEquals("C", (String) expr.getValue(todos)); + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/RemoveOperationTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/RemoveOperationTest.java new file mode 100644 index 000000000..7338a645f --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/RemoveOperationTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class RemoveOperationTest { + + @Test + public void removePropertyFromObject() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + new RemoveOperation("/1/description").perform(todos, Todo.class); + + assertNull(todos.get(1).getDescription()); + } + + @Test + public void removeItemFromList() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + new RemoveOperation("/1").perform(todos, Todo.class); + + assertEquals(2, todos.size()); + assertEquals("A", todos.get(0).getDescription()); + assertEquals("C", todos.get(1).getDescription()); + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/ReplaceOperationTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/ReplaceOperationTest.java new file mode 100644 index 000000000..1a6ad281f --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/ReplaceOperationTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class ReplaceOperationTest { + + @Test + public void replaceBooleanPropertyValue() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + ReplaceOperation replace = new ReplaceOperation("/1/complete", true); + replace.perform(todos, Todo.class); + + assertTrue(todos.get(1).isComplete()); + } + + @Test + public void replaceTextPropertyValue() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + ReplaceOperation replace = new ReplaceOperation("/1/description", "BBB"); + replace.perform(todos, Todo.class); + + assertEquals("BBB", todos.get(1).getDescription()); + } + + @Test + public void replaceTextPropertyValueWithANumber() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", false)); + todos.add(new Todo(3L, "C", false)); + + ReplaceOperation replace = new ReplaceOperation("/1/description", 22); + replace.perform(todos, Todo.class); + + assertEquals("22", todos.get(1).getDescription()); + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/TestOperationTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/TestOperationTest.java new file mode 100644 index 000000000..2a00c29ee --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/TestOperationTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class TestOperationTest { + + @Test + public void testPropertyValueEquals() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", true)); + todos.add(new Todo(3L, "C", false)); + + TestOperation test = new TestOperation("/0/complete", false); + test.perform(todos, Todo.class); + + TestOperation test2 = new TestOperation("/1/complete", true); + test2.perform(todos, Todo.class); + + } + + @Test(expected=PatchException.class) + public void testPropertyValueNotEquals() throws Exception { + // initial Todo list + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", true)); + todos.add(new Todo(3L, "C", false)); + + TestOperation test = new TestOperation("/0/complete", true); + test.perform(todos, Todo.class); + } + + @Test + public void testListElementEquals() throws Exception { + List todos = new ArrayList(); + todos.add(new Todo(1L, "A", false)); + todos.add(new Todo(2L, "B", true)); + todos.add(new Todo(3L, "C", false)); + + TestOperation test = new TestOperation("/1", new Todo(2L, "B", true)); + test.perform(todos, Todo.class); + + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/Todo.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/Todo.java new file mode 100644 index 000000000..01fee5d11 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/Todo.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.rest.webmvc.json.patch; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Roy Clarkson + * @author Craig Walls + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +class Todo { + + private Long id; + private String description; + private boolean complete; +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/TodoList.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/TodoList.java new file mode 100644 index 000000000..78d9b03f8 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/TodoList.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json.patch; + +import java.io.Serializable; +import java.util.List; + +public class TodoList implements Serializable { + + private static final long serialVersionUID = 1L; + + private List todos; + private Todo[] todoArray; + private String name; + + public List getTodos() { + return todos; + } + + public void setTodos(List todos) { + this.todos = todos; + } + + public Todo[] getTodoArray() { + return todoArray; + } + + public void setTodoArray(Todo[] todoArray) { + this.todoArray = todoArray; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/json/patch/patch-failing-operation-first.json b/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/json/patch/patch-failing-operation-first.json new file mode 100644 index 000000000..ad53fa6d0 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/json/patch/patch-failing-operation-first.json @@ -0,0 +1,7 @@ +[ + {"op":"test", "path":"/5/description", "value":"A"}, + {"op":"remove", "path":"/5"}, + {"op":"replace", "path":"/1/complete", "value":true}, + {"op":"copy", "path":"/3/description", "from":"/2/description"}, + {"op":"copy", "path":"/4", "from":"/0"} +] \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/json/patch/patch-failing-operation-in-middle.json b/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/json/patch/patch-failing-operation-in-middle.json new file mode 100644 index 000000000..13b604511 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/json/patch/patch-failing-operation-in-middle.json @@ -0,0 +1,8 @@ +[ + {"op":"test", "path":"/5/description", "value":"A"}, + {"op":"remove", "path":"/5"}, + {"op":"replace", "path":"/1/complete", "value":true}, + {"op":"test", "path":"/0/description", "value":"HUH"}, + {"op":"copy", "path":"/3/description", "from":"/2/description"}, + {"op":"copy", "path":"/4", "from":"/0"} +] \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/json/patch/patch-many-successful-operations.json b/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/json/patch/patch-many-successful-operations.json new file mode 100644 index 000000000..d8c09d577 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/json/patch/patch-many-successful-operations.json @@ -0,0 +1,8 @@ +[ + {"op":"test", "path":"/0", "value":{"id":1,"description":"A","complete":true}}, + {"op":"test", "path":"/5/description", "value":"F"}, + {"op":"remove", "path":"/5"}, + {"op":"replace", "path":"/1/complete", "value":true}, + {"op":"copy", "path":"/3/description", "from":"/2/description"}, + {"op":"copy", "path":"/4", "from":"/0"} +] \ No newline at end of file