DATAREST-885, DATAREST-885 - Cleanups in JSON Patch code.

This commit is contained in:
Oliver Gierke
2016-09-12 10:52:53 +02:00
parent 99aff8adf9
commit 06c15d3b6b
24 changed files with 257 additions and 261 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -16,26 +16,30 @@
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.
* 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
* @author Oliver Gierke
*/
public class AddOperation extends PatchOperation {
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);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.json.patch.PatchOperation#perform(java.lang.Object, java.lang.Class)
*/
@Override
<T> void perform(Object targetObject, Class<T> type) {
addValue(targetObject, evaluateValueFromTarget(targetObject, type));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -37,8 +37,9 @@ import static org.springframework.data.rest.webmvc.json.patch.PathToSpEL.*;
* </p>
*
* @author Craig Walls
* @author Oliver Gierke
*/
public class CopyOperation extends FromOperation {
class CopyOperation extends FromOperation {
/**
* Constructs the copy operation
@@ -50,9 +51,12 @@ public class CopyOperation extends FromOperation {
super("copy", path, from);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.json.patch.PatchOperation#perform(java.lang.Object, java.lang.Class)
*/
@Override
<T> void perform(Object target, Class<T> type) {
addValue(target, pathToExpression(from).getValue(target));
addValue(target, pathToExpression(getFrom()).getValue(target));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -16,16 +16,19 @@
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 <i>from</i> here to there.
* Abstract base class for operations requiring a source property, such as "copy" and "move". (e.g., copy <i>from</i>
* here to there.
*
* @author Craig Walls
* @author Oliver Gierke
*/
public abstract class FromOperation extends PatchOperation {
abstract class FromOperation extends PatchOperation {
private final String from;
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')
@@ -34,9 +37,8 @@ public abstract class FromOperation extends PatchOperation {
super(op, path);
this.from = from;
}
public String getFrom() {
return from;
}
}

View File

@@ -15,6 +15,9 @@
*/
package org.springframework.data.rest.webmvc.json.patch;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -23,23 +26,23 @@ import com.fasterxml.jackson.databind.ObjectMapper;
*
* @author Craig Walls
*/
@RequiredArgsConstructor
class JsonLateObjectEvaluator implements LateObjectEvaluator {
private static final ObjectMapper MAPPER = new ObjectMapper();
private JsonNode valueNode;
public JsonLateObjectEvaluator(JsonNode valueNode) {
this.valueNode = valueNode;
}
private final @NonNull ObjectMapper mapper;
private final @NonNull JsonNode 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);
return mapper.readValue(valueNode.traverse(), type);
} catch (Exception e) {
return null;
}
}
}

View File

@@ -15,13 +15,13 @@
*/
package org.springframework.data.rest.webmvc.json.patch;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.springframework.util.Assert;
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;
@@ -35,21 +35,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
* @author Oliver Gierke
* @author Mathias Düsterhöft
*/
@RequiredArgsConstructor
public class JsonPatchPatchConverter implements PatchConverter<JsonNode> {
private final ObjectMapper mapper;
/**
* Creates a new {@link JsonPatchPatchConverter} for the given {@link ObjectMapper}.
*
* @param mapper must not be {@literal null}.
*/
public JsonPatchPatchConverter(ObjectMapper mapper) {
Assert.notNull(mapper, "ObjectMapper must not be null!");
this.mapper = mapper;
}
private final @NonNull ObjectMapper mapper;
/**
* Constructs a {@link Patch} object given a JsonNode.
@@ -58,13 +47,16 @@ public class JsonPatchPatchConverter implements PatchConverter<JsonNode> {
* @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<PatchOperation> ops = new ArrayList<PatchOperation>(opNodes.size());
for (Iterator<JsonNode> elements = opNodes.elements(); elements.hasNext();) {
JsonNode opNode = elements.next();
String opType = opNode.get("op").textValue();
@@ -105,18 +97,25 @@ public class JsonPatchPatchConverter implements PatchConverter<JsonNode> {
List<PatchOperation> 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);
}
@@ -124,6 +123,7 @@ public class JsonPatchPatchConverter implements PatchConverter<JsonNode> {
}
private Object valueFromJsonNode(String path, JsonNode valueNode) {
if (valueNode == null || valueNode.isNull()) {
return null;
} else if (valueNode.isTextual()) {
@@ -137,36 +137,11 @@ public class JsonPatchPatchConverter implements PatchConverter<JsonNode> {
} else if (valueNode.isLong()) {
return valueNode.asLong();
} else if (valueNode.isObject()) {
return new JsonLateObjectEvaluator(valueNode);
return new JsonLateObjectEvaluator(mapper, 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;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -19,18 +19,15 @@ package org.springframework.data.rest.webmvc.json.patch;
* <p>
* Strategy interface for resolving values from an operation definition.
* </p>
*
* <p>
* {@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 <code>value</code> attribute of a JSON Patch operation will contain a JSON object. A different
* patch specification may define values in some non-JSON format.
* {@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 <code>value</code> attribute of a JSON Patch operation will
* contain a JSON object. A different patch specification may define values in some non-JSON format.
* </p>
*
* <p>
* This interface allows for pluggable evaluation of values, allowing {@link Patch} to remain
* independent of any specific patch representation.
* This interface allows for pluggable evaluation of values, allowing {@link Patch} to remain independent of any
* specific patch representation.
* </p>
*
* @author Craig Walls
@@ -38,5 +35,4 @@ package org.springframework.data.rest.webmvc.json.patch;
public interface LateObjectEvaluator {
<T> Object evaluate(Class<T> type);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -17,34 +17,37 @@ package org.springframework.data.rest.webmvc.json.patch;
/**
* <p>
* 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.
* 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.
* </p>
*
* <p>
* 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.
* 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.
* </p>
*
* @author Craig Walls
* @author Oliver Gierke
*/
public class MoveOperation extends FromOperation {
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);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.json.patch.PatchOperation#perform(java.lang.Object, java.lang.Class)
*/
@Override
<T> void perform(Object target, Class<T> type) {
addValue(target, popValueAtPath(target, from));
addValue(target, popValueAtPath(target, getFrom()));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -27,6 +27,7 @@ import java.util.List;
* </p>
*
* @author Craig Walls
* @author Oliver Gierke
*/
public class Patch {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.JsonNode;
* </p>
*
* @author Craig Walls
* @author Oliver Gierke
* @param <T> A type holding a representation of the patch. For example, a JsonNode if working with JSON Patch.
*/
public interface PatchConverter<T> {
@@ -48,5 +49,4 @@ public interface PatchConverter<T> {
* @return the patch representation object.
*/
T convert(Patch patch);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -19,6 +19,7 @@ package org.springframework.data.rest.webmvc.json.patch;
* Exception thrown if an error occurs in the course of applying a Patch.
*
* @author Craig Walls
* @author Oliver Gierke
*/
public class PatchException extends RuntimeException {
@@ -27,9 +28,8 @@ public class PatchException extends RuntimeException {
public PatchException(String message) {
super(message);
}
public PatchException(String message, Exception e) {
super(message, e);
}
}

View File

@@ -27,15 +27,13 @@ import org.springframework.expression.ExpressionException;
*
* @author Craig Walls
* @author Mathias Düsterhöft
* @author Oliver Gierke
*/
public abstract class PatchOperation {
protected final String op;
protected final String path;
protected final Object value;
protected final Expression spelExpression;
/**
@@ -57,6 +55,7 @@ public abstract class PatchOperation {
* {@link LateObjectEvaluator}.
*/
public PatchOperation(String op, String path, Object value) {
this.op = op;
this.path = path;
this.value = value;
@@ -92,17 +91,22 @@ public abstract class PatchOperation {
* @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);
@@ -117,17 +121,19 @@ public abstract class PatchOperation {
* @param target The target object.
* @param value The value to add.
*/
@SuppressWarnings({ "unchecked", "null" })
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<Object> list = (List<Object>) parentExpression.getValue(target);
int addAtIndex = listIndex >= 0 ? listIndex.intValue() : list.size();
list.add(addAtIndex, value);
list.add(listIndex >= 0 ? listIndex.intValue() : list.size(), value);
}
}
@@ -148,6 +154,7 @@ public abstract class PatchOperation {
* @return the value at the path on the given target object.
*/
protected Object getValueFromTarget(Object target) {
try {
return spelExpression.getValue(target);
} catch (ExpressionException e) {
@@ -177,11 +184,9 @@ public abstract class PatchOperation {
*/
abstract <T> void perform(Object target, Class<T> type);
// private helpers
private Integer targetListIndex(String path) {
String[] pathNodes = path.split("\\/");
String[] pathNodes = path.split("\\/");
String lastNode = pathNodes[pathNodes.length - 1];
if ("~".equals(lastNode)) {
@@ -194,5 +199,4 @@ public abstract class PatchOperation {
return null;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 the original author or authors.
* Copyright 2014-2016 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.
@@ -61,8 +61,6 @@ public class PathToSpEL {
return spelToExpression(pathNodesToSpEL(copyOf(path.split("\\/"), path.split("\\/").length - 1)));
}
// private helpers
private static String pathToSpEL(String path) {
return pathNodesToSpEL(path.split("\\/"));
}
@@ -71,7 +69,9 @@ public class PathToSpEL {
StringBuilder spelBuilder = new StringBuilder();
for (int i = 0; i < pathNodes.length; i++) {
String pathNode = pathNodes[i];
if (pathNode.length() == 0) {
continue;
}
@@ -82,20 +82,26 @@ public class PathToSpEL {
}
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;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -16,24 +16,29 @@
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.
* 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
* @author Oliver Gierke
*/
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);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.json.patch.PatchOperation#perform(java.lang.Object, java.lang.Class)
*/
@Override
<T> void perform(Object target, Class<T> type) {
popValueAtPath(target, path);
}
}

View File

@@ -24,16 +24,20 @@ 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);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.json.patch.PatchOperation#perform(java.lang.Object, java.lang.Class)
*/
@Override
<T> void perform(Object target, Class<T> type) {
setValueOnTarget(target, evaluateValueFromTarget(target, type));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -43,22 +43,29 @@ class TestOperation extends PatchOperation {
super("test", path, value);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.json.patch.PatchOperation#perform(java.lang.Object, java.lang.Class)
*/
@Override
<T> void perform(Object target, Class<T> 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;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -22,49 +22,47 @@ import java.util.List;
import org.junit.Test;
public class AddOperationTest {
public class AddOperationTests {
@Test
public void addBooleanPropertyValue() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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<Todo> todos = new ArrayList<Todo>();
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<Todo> todos = new ArrayList<Todo>();
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());
@@ -75,5 +73,4 @@ public class AddOperationTest {
assertEquals("C", todos.get(3).getDescription());
assertFalse(todos.get(3).isComplete());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -22,16 +22,16 @@ import java.util.List;
import org.junit.Test;
public class CopyOperationTest {
public class CopyOperationTests {
@Test
public void copyBooleanPropertyValue() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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);
@@ -40,12 +40,12 @@ public class CopyOperationTest {
@Test
public void copyStringPropertyValue() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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);
@@ -54,12 +54,12 @@ public class CopyOperationTest {
@Test
public void copyBooleanPropertyValueIntoStringProperty() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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);
@@ -68,83 +68,87 @@ public class CopyOperationTest {
@Test
public void copyListElementToBeginningOfList() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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(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<Todo> todos = new ArrayList<Todo>();
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(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<Todo> todos = new ArrayList<Todo>();
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(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<Todo> todos = new ArrayList<Todo>();
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
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<Todo> todos = new ArrayList<Todo>();
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
}
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
}
}

View File

@@ -36,11 +36,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* @author Oliver Gierke
* @author Mathias Düsterhöft
*/
public class JsonPatchTest {
public class JsonPatchTests {
@Test
public void manySuccessfulOperations() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
todos.add(new Todo(1L, "A", true));
todos.add(new Todo(2L, "B", false));
@@ -62,7 +62,7 @@ public class JsonPatchTest {
@Test
public void failureAtBeginning() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
todos.add(new Todo(1L, "A", true));
todos.add(new Todo(2L, "B", false));
@@ -80,7 +80,6 @@ public class JsonPatchTest {
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());
@@ -90,7 +89,7 @@ public class JsonPatchTest {
@Test
public void failureInMiddle() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
todos.add(new Todo(1L, "A", true));
todos.add(new Todo(2L, "B", false));
@@ -108,7 +107,6 @@ public class JsonPatchTest {
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());
@@ -117,11 +115,11 @@ public class JsonPatchTest {
}
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(mapper).convert(node);
JsonNode node = new ObjectMapper().readValue(resource.getInputStream(), JsonNode.class);
Patch patch = new JsonPatchPatchConverter(new ObjectMapper()).convert(node);
return patch;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -22,16 +22,16 @@ import java.util.List;
import org.junit.Test;
public class MoveOperationTest {
public class MoveOperationTests {
@Test
public void moveBooleanPropertyValue() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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);
@@ -45,12 +45,12 @@ public class MoveOperationTest {
@Test
public void moveStringPropertyValue() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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);
@@ -59,12 +59,12 @@ public class MoveOperationTest {
@Test
public void moveBooleanPropertyValueIntoStringProperty() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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);
@@ -77,23 +77,23 @@ public class MoveOperationTest {
//
// 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.
// 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<Todo> todos = new ArrayList<Todo>();
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());
@@ -102,53 +102,53 @@ public class MoveOperationTest {
@Test
public void moveListElementToMiddleOfList() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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<Todo> todos = new ArrayList<Todo>();
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<Todo> todos = new ArrayList<Todo>();
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<Todo> expected = new ArrayList<Todo>();
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);
@@ -156,19 +156,19 @@ public class MoveOperationTest {
@Test
public void moveListElementToEndOfList_usingTilde() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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<Todo> expected = new ArrayList<Todo>();
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);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -23,26 +23,31 @@ import java.util.List;
import org.junit.Test;
import org.springframework.expression.Expression;
public class PathToSpelTest {
public class PathToSpelTests {
@Test
public void listIndex() {
Expression expr = PathToSpEL.pathToExpression("/1/description");
List<Todo> todos = new ArrayList<Todo>();
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));
assertEquals("B", (String) expr.getValue(todos));
}
@Test
public void listTilde() {
Expression expr = PathToSpEL.pathToExpression("/~/description");
List<Todo> todos = new ArrayList<Todo>();
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));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -22,31 +22,31 @@ import java.util.List;
import org.junit.Test;
public class RemoveOperationTest {
public class RemoveOperationTests {
@Test
public void removePropertyFromObject() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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<Todo> todos = new ArrayList<Todo>();
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());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -24,11 +24,11 @@ import org.junit.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ReplaceOperationTest {
public class ReplaceOperationTests {
@Test
public void replaceBooleanPropertyValue() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
todos.add(new Todo(1L, "A", false));
todos.add(new Todo(2L, "B", false));
@@ -42,7 +42,7 @@ public class ReplaceOperationTest {
@Test
public void replaceTextPropertyValue() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
todos.add(new Todo(1L, "A", false));
todos.add(new Todo(2L, "B", false));
@@ -56,7 +56,7 @@ public class ReplaceOperationTest {
@Test
public void replaceTextPropertyValueWithANumber() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
todos.add(new Todo(1L, "A", false));
todos.add(new Todo(2L, "B", false));
@@ -76,8 +76,9 @@ public class ReplaceOperationTest {
Todo todo = new Todo(1L, "A", false);
ObjectMapper mapper = new ObjectMapper();
ReplaceOperation replace = new ReplaceOperation("/type",
new JsonLateObjectEvaluator(new ObjectMapper().readTree("{ \"value\" : \"new\" }")));
new JsonLateObjectEvaluator(mapper, mapper.readTree("{ \"value\" : \"new\" }")));
replace.perform(todo, Todo.class);
assertNotNull(todo.getType());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2016 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.
@@ -20,16 +20,16 @@ import java.util.List;
import org.junit.Test;
public class TestOperationTest {
public class TestOperationTests {
@Test
public void testPropertyValueEquals() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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);
@@ -38,28 +38,27 @@ public class TestOperationTest {
}
@Test(expected=PatchException.class)
@Test(expected = PatchException.class)
public void testPropertyValueNotEquals() throws Exception {
// initial Todo list
List<Todo> todos = new ArrayList<Todo>();
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<Todo> todos = new ArrayList<Todo>();
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);
}
}

View File

@@ -15,9 +15,12 @@
*/
package org.springframework.data.rest.webmvc.json.patch;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class TodoList implements Serializable {
private static final long serialVersionUID = 1L;
@@ -25,29 +28,4 @@ public class TodoList implements Serializable {
private List<Todo> todos;
private Todo[] todoArray;
private String name;
public List<Todo> getTodos() {
return todos;
}
public void setTodos(List<Todo> 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;
}
}