diff --git a/.gitignore b/.gitignore index 8d12d3a3..077b7c43 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,12 @@ target .idea META-INF -out -build +/out +/classes +/build .gradle - +.classpath +.project +.settings +/bin/ +/test-output \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0803b657..10a83322 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,7 @@ dependencies { version: "2.0.1"); compile(group: "com.github.fge", name: "jackson-coreutils", version: "1.6"); + compile(group: "com.amazonaws", name:"aws-java-sdk-dynamodb", version:"1.11.27"); testCompile(group: "org.testng", name: "testng", version: "6.8.7") { exclude(group: "junit", module: "junit"); exclude(group: "org.beanshell", module: "bsh"); @@ -64,6 +65,8 @@ dependencies { }; testCompile(group: "org.mockito", name: "mockito-core", version: "1.9.5"); testCompile(group: "org.assertj", name: "assertj-core", version: "1.7.0"); + testCompile(group:"com.jayway.jsonpath", name:"json-path-assert", version:"2.2.0"); + testCompile(group: "com.amazonaws", name:"DynamoDBLocal", version:"1.11.0.1"); } javadoc.options.links("http://docs.oracle.com/javase/6/docs/api/"); @@ -80,6 +83,26 @@ javadoc.options.links("http://fge.github.io/jackson-coreutils/"); */ repositories { mavenCentral(); + maven { + url 'http://dynamodb-local.s3-website-us-west-2.amazonaws.com/release' + } + +} + +task copyNativeDeps(type: Copy) { + from (configurations.testCompile) { + include "*.dylib" + include "*.so" + include "*.dll" + } + into 'build/libs' +} + +test.dependsOn copyNativeDeps +test.doFirst { + systemProperty "java.library.path", 'build/libs' + environment "DYLD_LIBRARY_PATH", './build/libs' + environment "LD_LIBRARY_PATH", './build/libs' } /* diff --git a/src/main/java/com/amazonaws/services/dynamodbv2/xspec/NULLComparable.java b/src/main/java/com/amazonaws/services/dynamodbv2/xspec/NULLComparable.java new file mode 100644 index 00000000..d1159036 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodbv2/xspec/NULLComparable.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015-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. + * 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 com.amazonaws.services.dynamodbv2.xspec; + +import com.amazonaws.services.dynamodbv2.xspec.NULL; + +/** + * TODO for daisuke + * + * @since 0.13 + * @version $Id$ + * @author Alexander Patrikalakis + */ +public class NULLComparable extends NULL { + public NULLComparable(String path) { + super(path); + } + public ComparatorCondition eq(Operand that) { + return new ComparatorCondition("=", this, that); + } + public static final LiteralOperand generateNull() { + return new LiteralOperand((Object) null); + } +} diff --git a/src/main/java/com/amazonaws/services/dynamodbv2/xspec/PathSetAction.java b/src/main/java/com/amazonaws/services/dynamodbv2/xspec/PathSetAction.java new file mode 100644 index 00000000..3de9a967 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodbv2/xspec/PathSetAction.java @@ -0,0 +1,29 @@ +/* + * Copyright 2015-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. + * 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 com.amazonaws.services.dynamodbv2.xspec; + +/** + * TODO for daisuke + * + * @since 0.13 + * @version $Id$ + * @author Alexander Patrikalakis + */ +public class PathSetAction extends UpdateAction { + public PathSetAction(PathOperand attr, PathOperand value) { + super("SET", attr, value); + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/AddOperation.java b/src/main/java/com/github/fge/jsonpatch/AddOperation.java index 56716fb0..07a02688 100644 --- a/src/main/java/com/github/fge/jsonpatch/AddOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/AddOperation.java @@ -1,5 +1,7 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) * * This software is dual-licensed under: * @@ -19,6 +21,7 @@ package com.github.fge.jsonpatch; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; @@ -28,6 +31,7 @@ import com.github.fge.jackson.jsonpointer.ReferenceToken; import com.github.fge.jackson.jsonpointer.TokenResolver; import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; /** @@ -99,6 +103,17 @@ public JsonNode apply(final JsonNode node) ? addToArray(path, node) : addToObject(path, node); } + + @Override + public void applyToBuilder(ExpressionSpecBuilder builder) { + final TokenResolver node = Iterators.getLast(path.iterator(), null /*default*/); + if(null == node || "-".equals(node.getToken().getRaw())) { + //list_append + throw new UnsupportedOperationException("list_append not supported yet"); + } else { + super.applyToBuilder(builder); + } + } private JsonNode addToArray(final JsonPointer path, final JsonNode node) throws JsonPatchException @@ -133,7 +148,7 @@ private JsonNode addToObject(final JsonPointer path, final JsonNode node) { final JsonNode ret = node.deepCopy(); final ObjectNode target = (ObjectNode) path.parent().get(ret); - target.put(Iterables.getLast(path).getToken().getRaw(), value); + target.set(Iterables.getLast(path).getToken().getRaw(), value); return ret; } } diff --git a/src/main/java/com/github/fge/jsonpatch/CopyOperation.java b/src/main/java/com/github/fge/jsonpatch/CopyOperation.java index 9a5a75b9..4b1922f6 100644 --- a/src/main/java/com/github/fge/jsonpatch/CopyOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/CopyOperation.java @@ -1,5 +1,7 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) * * This software is dual-licensed under: * @@ -19,6 +21,8 @@ package com.github.fge.jsonpatch; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; +import com.amazonaws.services.dynamodbv2.xspec.PathSetAction; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; @@ -60,4 +64,13 @@ public JsonNode apply(final JsonNode node) "jsonPatch.noSuchPath")); return new AddOperation(path, dupData).apply(node); } + + @Override + public void applyToBuilder(ExpressionSpecBuilder builder) { + String copyPath = pathGenerator.apply(from); + String setPath = pathGenerator.apply(path); + //set the attribute in the path location + builder.addUpdate(new PathSetAction(ExpressionSpecBuilder.attribute(setPath), + ExpressionSpecBuilder.attribute(copyPath))); + } } diff --git a/src/main/java/com/github/fge/jsonpatch/JsonPatch.java b/src/main/java/com/github/fge/jsonpatch/JsonPatch.java index 178ab867..75509b30 100644 --- a/src/main/java/com/github/fge/jsonpatch/JsonPatch.java +++ b/src/main/java/com/github/fge/jsonpatch/JsonPatch.java @@ -1,5 +1,7 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) * * This software is dual-licensed under: * @@ -19,6 +21,7 @@ package com.github.fge.jsonpatch; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonNode; @@ -28,6 +31,7 @@ import com.github.fge.jackson.JacksonUtils; import com.github.fge.msgsimple.bundle.MessageBundle; import com.github.fge.msgsimple.load.MessageBundles; +import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -89,8 +93,8 @@ *

IMPORTANT NOTE: the JSON Patch is supposed to be VALID when the * constructor for this class ({@link JsonPatch#fromJson(JsonNode)} is used.

*/ -public final class JsonPatch - implements JsonSerializable +public class JsonPatch + implements JsonSerializable, Supplier { private static final MessageBundle BUNDLE = MessageBundles.getBundle(JsonPatchMessages.class); @@ -98,7 +102,7 @@ public final class JsonPatch /** * List of operations */ - private final List operations; + protected final List operations; /** * Constructor @@ -126,7 +130,7 @@ public static JsonPatch fromJson(final JsonNode node) throws IOException { BUNDLE.checkNotNull(node, "jsonPatch.nullInput"); - return JacksonUtils.getReader().withType(JsonPatch.class) + return JacksonUtils.getReader().forType(JsonPatch.class) .readValue(node); } @@ -148,6 +152,19 @@ public JsonNode apply(final JsonNode node) return ret; } + + /** + * Converts this JsonPatch into an ExpressionSpecBuilder + * @return an expression spec builder that contains the updates contained in this + * patch + */ + public ExpressionSpecBuilder get() { + ExpressionSpecBuilder builder = new ExpressionSpecBuilder(); + for(JsonPatchOperation operation : operations) { + operation.applyToBuilder(builder); + } + return builder; + } @Override public String toString() diff --git a/src/main/java/com/github/fge/jsonpatch/JsonPatchException.java b/src/main/java/com/github/fge/jsonpatch/JsonPatchException.java index 088f1b87..a283061b 100644 --- a/src/main/java/com/github/fge/jsonpatch/JsonPatchException.java +++ b/src/main/java/com/github/fge/jsonpatch/JsonPatchException.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) * * This software is dual-licensed under: * @@ -19,6 +20,7 @@ package com.github.fge.jsonpatch; +@SuppressWarnings("serial") public final class JsonPatchException extends Exception { diff --git a/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java b/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java index a5ad5bd9..f0efb41e 100644 --- a/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java @@ -1,5 +1,7 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) * * This software is dual-licensed under: * @@ -27,10 +29,13 @@ import com.github.fge.jackson.jsonpointer.JsonPointer; import com.github.fge.msgsimple.bundle.MessageBundle; import com.github.fge.msgsimple.load.MessageBundles; +import com.google.common.base.Function; import static com.fasterxml.jackson.annotation.JsonSubTypes.*; import static com.fasterxml.jackson.annotation.JsonTypeInfo.*; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; + @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "op") @JsonSubTypes({ @@ -61,6 +66,8 @@ public abstract class JsonPatchOperation { protected static final MessageBundle BUNDLE = MessageBundles.getBundle(JsonPatchMessages.class); + + Function pathGenerator = new JsonPathToAttributePath(); protected final String op; @@ -94,6 +101,12 @@ protected JsonPatchOperation(final String op, final JsonPointer path) public abstract JsonNode apply(final JsonNode node) throws JsonPatchException; + /** + * Apply the current patch operation to an update expression builder + * @param builder the builder to apply this expression to + */ + public abstract void applyToBuilder(ExpressionSpecBuilder builder); + @Override public abstract String toString(); } diff --git a/src/main/java/com/github/fge/jsonpatch/JsonPathToAttributePath.java b/src/main/java/com/github/fge/jsonpatch/JsonPathToAttributePath.java new file mode 100644 index 00000000..ddcda436 --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/JsonPathToAttributePath.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) + * + * This software is dual-licensed under: + * + * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any + * later version; + * - the Apache Software License (ASL) version 2.0. + * + * The text of this file and of both licenses is available at the root of this + * project or, if you have the jar distribution, in directory META-INF/, under + * the names LGPL-3.0.txt and ASL-2.0.txt respectively. + * + * Direct link to the sources: + * + * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt + * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package com.github.fge.jsonpatch; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jackson.jsonpointer.JsonPointer; +import com.github.fge.jackson.jsonpointer.TokenResolver; +import com.google.common.base.Function; +import com.google.common.base.Joiner; + +public class JsonPathToAttributePath implements Function { + + private static Pattern ARRAY_PATTERN = Pattern.compile("(0|[1-9][0-9]+)"); + + + @Override + public String apply(JsonPointer pointer) { + List elements = new ArrayList(); + for (TokenResolver tokenResolver : pointer) { + String token = tokenResolver.getToken().getRaw(); + if (ARRAY_PATTERN.matcher(token).matches()) { + String last = elements.get(elements.size() - 1); + elements.set(elements.size() - 1, String.format("%s[%s]", last, token)); + } else { + elements.add(token); + } + } + + return Joiner.on(".").join(elements); + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/MoveOperation.java b/src/main/java/com/github/fge/jsonpatch/MoveOperation.java index 405b737c..edeb2632 100644 --- a/src/main/java/com/github/fge/jsonpatch/MoveOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/MoveOperation.java @@ -1,5 +1,7 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) * * This software is dual-licensed under: * @@ -19,6 +21,8 @@ package com.github.fge.jsonpatch; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; +import com.amazonaws.services.dynamodbv2.xspec.PathSetAction; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; @@ -86,4 +90,15 @@ public JsonNode apply(final JsonNode node) final JsonPatchOperation add = new AddOperation(path, movedNode); return add.apply(remove.apply(node)); } + + @Override + public void applyToBuilder(ExpressionSpecBuilder builder) { + String removePath = pathGenerator.apply(from); + String setPath = pathGenerator.apply(path); + //remove the attribute in the from location + builder.addUpdate(ExpressionSpecBuilder.remove(removePath)); + //set the attribute in the path location + builder.addUpdate(new PathSetAction(ExpressionSpecBuilder.attribute(setPath), + ExpressionSpecBuilder.attribute(removePath))); + } } diff --git a/src/main/java/com/github/fge/jsonpatch/PathValueOperation.java b/src/main/java/com/github/fge/jsonpatch/PathValueOperation.java index f9e1c86f..7652cd11 100644 --- a/src/main/java/com/github/fge/jsonpatch/PathValueOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/PathValueOperation.java @@ -1,5 +1,7 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) * * This software is dual-licensed under: * @@ -19,15 +21,23 @@ package com.github.fge.jsonpatch; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.node.JsonNodeType; import com.github.fge.jackson.jsonpointer.JsonPointer; import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; /** * Base class for patch operations taking a value in addition to a path @@ -72,6 +82,100 @@ public final void serializeWithType(final JsonGenerator jgen, { serialize(jgen, provider); } + + @Override + public void applyToBuilder(ExpressionSpecBuilder builder) { + String attributePath = pathGenerator.apply(path); + JsonNodeType type = value.getNodeType(); + switch (type) { + case NUMBER: + builder.addUpdate(ExpressionSpecBuilder.N(attributePath).set(value.numberValue())); + break; + + case STRING: + builder.addUpdate(ExpressionSpecBuilder.S(attributePath).set(value.textValue())); + break; + + case BOOLEAN: + builder.addUpdate(ExpressionSpecBuilder.BOOL(attributePath).set(value.booleanValue())); + break; + + case NULL: + builder.addUpdate(ExpressionSpecBuilder.NULL(attributePath).set()); + break; + + case ARRAY: + if (value.iterator().hasNext() == false) { + builder.addUpdate(ExpressionSpecBuilder.L(attributePath).set(Collections.emptyList())); + } else { + JsonNode repNode = value.iterator().next(); + if (repNode.isNumber()) { + builder.addUpdate(ExpressionSpecBuilder.NS(attributePath).set(convertNumberList(value))); + } else if (repNode.isTextual()) { + builder.addUpdate(ExpressionSpecBuilder.SS(attributePath).set(convertStringList(value))); + } else { + throw new UnsupportedOperationException("Not implemented yet: " + repNode.getNodeType()); + } + } + break; + + case OBJECT: + Map m = toMap(value); + builder.addUpdate(ExpressionSpecBuilder.M(attributePath).set(m)); + break; + + default: + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Not implemented yet: " + type); + } + } + + private static Map toMap(JsonNode value) { + Map m = new LinkedHashMap(); + for (Iterator> iterator = value.fields(); iterator.hasNext();) { + Map.Entry e = iterator.next(); + JsonNodeType nodeType = e.getValue().getNodeType(); + if (nodeType.equals(JsonNodeType.OBJECT)) { + m.put(e.getKey(), toMap(e.getValue())); + } else if (nodeType.equals(JsonNodeType.BOOLEAN)) { + m.put(e.getKey(), e.getValue().booleanValue()); + } else if (nodeType.equals(JsonNodeType.NUMBER)) { + m.put(e.getKey(), e.getValue().numberValue()); + } else if (nodeType.equals(JsonNodeType.STRING)) { + m.put(e.getKey(), e.getValue().textValue()); + } else if (nodeType.equals(JsonNodeType.ARRAY)) { + if (e.getValue().iterator().hasNext() == false) { + m.put(e.getKey(), Collections.emptyList()); + } else { + JsonNode repNode = e.getValue().iterator().next(); + if (repNode.isNumber()) { + m.put(e.getKey(), convertNumberList(e.getValue())); + } else if (repNode.isTextual()) { + m.put(e.getKey(), convertStringList(e.getValue())); + } else { + throw new UnsupportedOperationException("Not implemented yet: " + repNode.getNodeType()); + } + } + } + } + return m; + } + + private static Set convertNumberList(JsonNode parent) { + Set ns = new HashSet(); + for(JsonNode node : parent) { + ns.add(node.numberValue()); + } + return ns; + } + + private static Set convertStringList(JsonNode parent) { + Set ns = new HashSet(); + for(JsonNode node : parent) { + ns.add(node.textValue()); + } + return ns; + } @Override public final String toString() diff --git a/src/main/java/com/github/fge/jsonpatch/RemoveOperation.java b/src/main/java/com/github/fge/jsonpatch/RemoveOperation.java index 16b0011d..e199a42c 100644 --- a/src/main/java/com/github/fge/jsonpatch/RemoveOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/RemoveOperation.java @@ -1,5 +1,7 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) * * This software is dual-licensed under: * @@ -19,6 +21,7 @@ package com.github.fge.jsonpatch; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; @@ -67,6 +70,15 @@ public JsonNode apply(final JsonNode node) ((ArrayNode) parentNode).remove(Integer.parseInt(raw)); return ret; } + + @Override + public void applyToBuilder(ExpressionSpecBuilder builder) { + String attributePath = pathGenerator.apply(path); + builder.addUpdate(ExpressionSpecBuilder.remove(attributePath)); + //because it is an error to remove a path that does not exist + //add an attribute_exists() condition + builder.withCondition(ExpressionSpecBuilder.attribute_exists(pathGenerator.apply(path))); + } @Override public void serialize(final JsonGenerator jgen, diff --git a/src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java b/src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java index 72405a6a..8a1f4b78 100644 --- a/src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java @@ -1,5 +1,7 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) * * This software is dual-licensed under: * @@ -19,6 +21,7 @@ package com.github.fge.jsonpatch; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; @@ -45,6 +48,15 @@ public ReplaceOperation(@JsonProperty("path") final JsonPointer path, { super("replace", path, value); } + + @Override + public void applyToBuilder(ExpressionSpecBuilder builder) { + //add the set operation + super.applyToBuilder(builder); + //because it is an error to replace a path that does not exist + //add an attribute_exists() condition + builder.withCondition(ExpressionSpecBuilder.attribute_exists(pathGenerator.apply(path))); + } @Override public JsonNode apply(final JsonNode node) @@ -73,7 +85,7 @@ public JsonNode apply(final JsonNode node) final JsonNode parent = path.parent().get(ret); final String rawToken = Iterables.getLast(path).getToken().getRaw(); if (parent.isObject()) - ((ObjectNode) parent).put(rawToken, replacement); + ((ObjectNode) parent).replace(rawToken, replacement); else ((ArrayNode) parent).set(Integer.parseInt(rawToken), replacement); return ret; diff --git a/src/main/java/com/github/fge/jsonpatch/TestOperation.java b/src/main/java/com/github/fge/jsonpatch/TestOperation.java index c6b3fa54..d5a42766 100644 --- a/src/main/java/com/github/fge/jsonpatch/TestOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/TestOperation.java @@ -1,5 +1,7 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) * * This software is dual-licensed under: * @@ -19,9 +21,12 @@ package com.github.fge.jsonpatch; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; +import com.amazonaws.services.dynamodbv2.xspec.NULLComparable; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; import com.github.fge.jackson.JsonNumEquals; import com.github.fge.jackson.jsonpointer.JsonPointer; import com.google.common.base.Equivalence; @@ -65,4 +70,37 @@ public JsonNode apply(final JsonNode node) "jsonPatch.valueTestFailure")); return node.deepCopy(); } + + @Override + public void applyToBuilder(ExpressionSpecBuilder builder) { + String attributePath = pathGenerator.apply(path); + JsonNodeType type = value.getNodeType(); + switch (type) { + case NUMBER: + builder.withCondition(ExpressionSpecBuilder.N(attributePath).eq(value.numberValue())); + break; + + case STRING: + builder.withCondition(ExpressionSpecBuilder.S(attributePath).eq(value.textValue())); + break; + + case BOOLEAN: + builder.withCondition(ExpressionSpecBuilder.BOOL(attributePath).eq(value.booleanValue())); + break; + + case NULL: + builder.withCondition(new NULLComparable(attributePath).eq(NULLComparable.generateNull())); + break; + + case ARRAY: + throw new UnsupportedOperationException("DynamoDB only supports conditions on scalars, not lists"); + + case OBJECT: + throw new UnsupportedOperationException("DynamoDB only supports conditions on scalars, not maps"); + + default: + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Not implemented yet: " + type); + } + } } diff --git a/src/main/java/com/github/fge/jsonpatch/mergepatch/ObjectMergePatch.java b/src/main/java/com/github/fge/jsonpatch/mergepatch/ObjectMergePatch.java index 39f79d65..91ec7844 100644 --- a/src/main/java/com/github/fge/jsonpatch/mergepatch/ObjectMergePatch.java +++ b/src/main/java/com/github/fge/jsonpatch/mergepatch/ObjectMergePatch.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) * * This software is dual-licensed under: * @@ -84,7 +85,7 @@ public JsonNode apply(final JsonNode input) */ value = Optional.fromNullable(ret.get(key)) .or(NullNode.getInstance()); - ret.put(key, entry.getValue().apply(value)); + ret.set(key, entry.getValue().apply(value)); } ret.remove(removedMembers); diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchOperationTest.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchOperationTest.java index bc6aa906..9d0f4aa2 100644 --- a/src/test/java/com/github/fge/jsonpatch/JsonPatchOperationTest.java +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchOperationTest.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) * * This software is dual-licensed under: * @@ -57,7 +58,7 @@ protected JsonPatchOperationTest(final String prefix) final JsonNode node = JsonLoader.fromResource(resource); errors = node.get("errors"); ops = node.get("ops"); - reader = JacksonUtils.getReader().withType(JsonPatchOperation.class); + reader = JacksonUtils.getReader().forType(JsonPatchOperation.class); } @DataProvider diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchTest.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchTest.java index 30eadf30..e00a968a 100644 --- a/src/test/java/com/github/fge/jsonpatch/JsonPatchTest.java +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchTest.java @@ -1,5 +1,7 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) * * This software is dual-licensed under: * @@ -22,6 +24,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.github.fge.jackson.JacksonUtils; +import com.github.fge.jackson.JsonLoader; +import com.github.fge.jackson.jsonpointer.JsonPointer; import com.github.fge.msgsimple.bundle.MessageBundle; import com.github.fge.msgsimple.load.MessageBundles; import com.google.common.collect.ImmutableList; @@ -119,4 +123,19 @@ public void whenOneOperationFailsNextOperationIsNotCalled() verifyZeroInteractions(op2); } + + @Test + public void testSingleAdd() throws Exception { + // setup + String patchExpression = "[ { \"op\": \"add\", \"path\": \"/a\", \"value\": 1 } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + // exercise + JsonPatch actual = JsonPatch.fromJson(jsonNode); + // verify + assertEquals(actual.operations.size(), 1); + AddOperation operation = (AddOperation) actual.operations.get(0); + assertEquals(operation.op, "add"); + assertEquals(operation.path, new JsonPointer("/a")); + assertEquals(operation.value, JsonLoader.fromString("1")); + } } diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecAdd.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecAdd.java new file mode 100644 index 00000000..ae14e095 --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecAdd.java @@ -0,0 +1,123 @@ +package com.github.fge.jsonpatch; + +import java.math.BigDecimal; + +import org.testng.Assert; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.document.Item; +import com.amazonaws.services.dynamodbv2.document.PrimaryKey; +import com.amazonaws.services.dynamodbv2.document.Table; +import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded; +import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import com.amazonaws.services.dynamodbv2.model.KeyType; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; +import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; +import com.amazonaws.services.dynamodbv2.xspec.UpdateItemExpressionSpec; +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jackson.JsonLoader; +import com.google.common.collect.ImmutableMap; + +public class JsonPatchToXSpecAdd { + private static final String KEY_ATTRIBUTE_NAME = "key"; + + private static final String VALUE = "keyValue"; + + private static final PrimaryKey PK = new PrimaryKey(KEY_ATTRIBUTE_NAME, VALUE); + + private static final String TABLE_NAME = "json_patch_test"; + + private Table table; + + + @BeforeTest + public void setUp() throws Exception { + AmazonDynamoDB amazonDynamoDB = DynamoDBEmbedded.create().amazonDynamoDB(); + try { + amazonDynamoDB.deleteTable(TABLE_NAME); + } catch(ResourceNotFoundException e) { + //do nothing because the first run will not have the table. + } + amazonDynamoDB.createTable(new CreateTableRequest() + .withTableName(TABLE_NAME) + .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L)) + .withAttributeDefinitions(new AttributeDefinition() + .withAttributeName(KEY_ATTRIBUTE_NAME) + .withAttributeType(ScalarAttributeType.S)) + .withKeySchema(new KeySchemaElement() + .withAttributeName(KEY_ATTRIBUTE_NAME) + .withKeyType(KeyType.HASH))); + table = new Table(amazonDynamoDB, TABLE_NAME); + } + + @Test + public void testAddSinglePathNumber() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .build())); + String patchExpression = "[ { \"op\": \"add\", \"path\": \"/a\", \"value\": 1 } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertEquals(item.getNumber("a").longValue(), 1L); + } + + @Test + public void testAddNestedPathString() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", ImmutableMap.of("a", 1L)) + .build())); + + String patchExpression = "[ { \"op\": \"add\", \"path\": \"/a/b\", \"value\": \"foo\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertTrue(item.getRawMap("a").containsKey("a")); + Assert.assertEquals(((BigDecimal) item.getMap("a").get("a")).longValue(), 1L); + Assert.assertTrue(item.getMap("a").containsKey("b")); + Assert.assertEquals(item.getMap("a").get("b"), "foo"); + } + + @Test + public void createItemWithJsonPatch() throws Exception { + // setup + String patchExpression = "[ { \"op\": \"add\", \"path\": \"/a\", \"value\": \"b\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec);//throw + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertEquals(item.getString("a"), "b"); + } +} diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecRemove.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecRemove.java new file mode 100644 index 00000000..618bf551 --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecRemove.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) + * + * This software is dual-licensed under: + * + * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any + * later version; + * - the Apache Software License (ASL) version 2.0. + * + * The text of this file and of both licenses is available at the root of this + * project or, if you have the jar distribution, in directory META-INF/, under + * the names LGPL-3.0.txt and ASL-2.0.txt respectively. + * + * Direct link to the sources: + * + * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt + * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package com.github.fge.jsonpatch; + +import org.testng.Assert; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.document.Item; +import com.amazonaws.services.dynamodbv2.document.PrimaryKey; +import com.amazonaws.services.dynamodbv2.document.Table; +import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded; +import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import com.amazonaws.services.dynamodbv2.model.KeyType; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; +import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; +import com.amazonaws.services.dynamodbv2.xspec.UpdateItemExpressionSpec; +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jackson.JsonLoader; +import com.google.common.collect.ImmutableMap; + +public class JsonPatchToXSpecRemove { + private static final String TABLE_NAME = "json_patch_test"; + + private static final String KEY_ATTRIBUTE_NAME = "key"; + + private static final String VALUE = "keyValue"; + + private static final PrimaryKey PK = new PrimaryKey(KEY_ATTRIBUTE_NAME, VALUE); + + private Table table; + + + @BeforeTest + public void setUp() throws Exception { + AmazonDynamoDB amazonDynamoDB = DynamoDBEmbedded.create().amazonDynamoDB(); + try { + amazonDynamoDB.deleteTable(TABLE_NAME); + } catch(ResourceNotFoundException e) { + //do nothing because the first run will not have the table. + } + amazonDynamoDB.createTable(new CreateTableRequest() + .withTableName(TABLE_NAME) + .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L)) + .withAttributeDefinitions(new AttributeDefinition() + .withAttributeName(KEY_ATTRIBUTE_NAME) + .withAttributeType(ScalarAttributeType.S)) + .withKeySchema(new KeySchemaElement() + .withAttributeName(KEY_ATTRIBUTE_NAME) + .withKeyType(KeyType.HASH))); + table = new Table(amazonDynamoDB, TABLE_NAME); + } + + @Test + public void test_remove_singlePath() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", ImmutableMap.of("a", 2, "b", true)) + .build())); + + // setup + String patchExpression = "[ { \"op\": \"remove\", \"path\": \"/a\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder actual = jsonPatch.get(); + UpdateItemExpressionSpec actualSpec = actual.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, actualSpec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertFalse(item.hasAttribute("a")); + } + + @Test + public void test_remove_nestedPath() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", ImmutableMap.of("a", 2, "b", true)) + .build())); + + // setup + String patchExpression = "[ { \"op\": \"remove\", \"path\": \"/a/a\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder actual = jsonPatch.get(); + UpdateItemExpressionSpec actualSpec = actual.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, actualSpec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertTrue(item.getRawMap("a").containsKey("b")); + Assert.assertEquals(item.getRawMap("a").get("b"), true); + Assert.assertFalse(item.getRawMap("a").containsKey("a")); + } + + @Test + public void test_remove_absentPath() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", "b") + .build())); + + // setup + String patchExpression = "[ { \"op\": \"remove\", \"path\": \"/c\" } ]"; // $.c does not exist in target + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder actual = jsonPatch.get(); + UpdateItemExpressionSpec actualSpec = actual.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, actualSpec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertEquals(item.getString("a"), "b"); + } + + @Test(expectedExceptions = AmazonServiceException.class) + public void test_remove_absentObjectPath() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", "b") + .build())); + + // setup + String patchExpression = "[ { \"op\": \"remove\", \"path\": \"/c/d\" } ]"; // $.c does not exist in target + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder actual = jsonPatch.get(); + UpdateItemExpressionSpec actualSpec = actual.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, actualSpec); + } +} diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecReplace.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecReplace.java new file mode 100644 index 00000000..95013de0 --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecReplace.java @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) + * + * This software is dual-licensed under: + * + * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any + * later version; + * - the Apache Software License (ASL) version 2.0. + * + * The text of this file and of both licenses is available at the root of this + * project or, if you have the jar distribution, in directory META-INF/, under + * the names LGPL-3.0.txt and ASL-2.0.txt respectively. + * + * Direct link to the sources: + * + * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt + * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package com.github.fge.jsonpatch; + +import java.math.BigDecimal; + +import org.testng.Assert; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.document.Item; +import com.amazonaws.services.dynamodbv2.document.PrimaryKey; +import com.amazonaws.services.dynamodbv2.document.Table; +import com.amazonaws.services.dynamodbv2.document.UpdateItemOutcome; +import com.amazonaws.services.dynamodbv2.document.internal.InternalUtils; +import com.amazonaws.services.dynamodbv2.document.spec.UpdateItemSpec; +import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded; +import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import com.amazonaws.services.dynamodbv2.model.KeyType; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; +import com.amazonaws.services.dynamodbv2.model.ReturnValue; +import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; +import com.amazonaws.services.dynamodbv2.xspec.UpdateItemExpressionSpec; +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jackson.JsonLoader; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +public class JsonPatchToXSpecReplace { + + private static final String KEY_ATTRIBUTE_NAME = "key"; + + private static final String VALUE = "keyValue"; + + private static final PrimaryKey PK = new PrimaryKey(KEY_ATTRIBUTE_NAME, VALUE); + + private static final String TABLE_NAME = "json_patch_test"; + + private Table table; + + + @BeforeTest + public void setUp() throws Exception { + AmazonDynamoDB amazonDynamoDB = DynamoDBEmbedded.create().amazonDynamoDB(); + try { + amazonDynamoDB.deleteTable(TABLE_NAME); + } catch(ResourceNotFoundException e) { + //do nothing because the first run will not have the table. + } + amazonDynamoDB.createTable(new CreateTableRequest() + .withTableName(TABLE_NAME) + .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L)) + .withAttributeDefinitions(new AttributeDefinition() + .withAttributeName(KEY_ATTRIBUTE_NAME) + .withAttributeType(ScalarAttributeType.S)) + .withKeySchema(new KeySchemaElement() + .withAttributeName(KEY_ATTRIBUTE_NAME) + .withKeyType(KeyType.HASH))); + table = new Table(amazonDynamoDB, TABLE_NAME); + } + + @Test(expectedExceptions = ConditionalCheckFailedException.class) + public void testReplaceSinglePathNumberNonextant() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .build())); + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": 1 } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + // verify + table.getItem(PK); //throw + } + + @Test + public void testReplaceSinglePathNumberExtant() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", "peekaboo") + .build())); + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": 1 } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + UpdateItemOutcome out = table.updateItem(new UpdateItemSpec() + .withPrimaryKey(KEY_ATTRIBUTE_NAME, VALUE) + .withExpressionSpec(spec) + .withReturnValues(ReturnValue.ALL_OLD)); + + Item oldItem = Item.fromMap(InternalUtils.toSimpleMapValue(out.getUpdateItemResult().getAttributes())); + Assert.assertTrue(oldItem.hasAttribute("a")); + Assert.assertEquals(oldItem.getString("a"), "peekaboo"); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertEquals(item.getNumber("a").longValue(), 1L); + } + + @Test(expectedExceptions = ConditionalCheckFailedException.class) + public void testReplaceNestedPathString() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", ImmutableMap.of("a", 1L)) + .build())); + + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a/b\", \"value\": \"foo\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + } + + @Test + public void test_replace_existingNestedPath_string() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", ImmutableMap.of("a", 2L, "b", true)) + .build())); + + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a/b\", \"value\": \"bar\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertTrue(item.getRawMap("a").containsKey("a")); + Assert.assertEquals(((BigDecimal) item.getRawMap("a").get("a")).longValue(), 2L); + Assert.assertTrue(item.getRawMap("a").containsKey("b")); + Assert.assertEquals(item.getRawMap("a").get("b"), "bar"); + } + + @Test(expectedExceptions = AmazonServiceException.class) + public void test_replace_property_toScalar_string() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", 1L) + .build())); + + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a/b\", \"value\": \"bar\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + } + + @Test + public void test_replace_singlePath_stringSet() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", 1L) + .build())); + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": [\"foo\",\"bar\"] } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertTrue(item.getList("a").contains("foo")); + Assert.assertTrue(item.getList("a").contains("bar")); + } + + @Test + public void test_replace_replaceExisting_singlePath_stringSet() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", ImmutableSet.of("foo", "bar")) + .build())); + + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": [\"baz\",\"qux\"] } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertEquals(item.getList("a").size(), 2); + Assert.assertTrue(item.getList("a").contains("baz")); + Assert.assertTrue(item.getList("a").contains("qux")); + } + + @Test + public void test_replace_singlePath_object() throws Exception { + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", 1L) + .build())); + // setup + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": {\"b\": \"c\", \"d\": 1} } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertTrue(item.getRawMap("a").containsKey("b")); + Assert.assertEquals(item.getRawMap("a").get("b"), "c"); + Assert.assertTrue(item.getRawMap("a").containsKey("d")); + Assert.assertEquals(((BigDecimal) item.getRawMap("a").get("d")).longValue(), 1L); + } +} diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecTest.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecTest.java new file mode 100644 index 00000000..6d2bc2f9 --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecTest.java @@ -0,0 +1,95 @@ +package com.github.fge.jsonpatch; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; +import com.amazonaws.services.dynamodbv2.xspec.UpdateItemExpressionSpec; +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jackson.JsonLoader; + +public class JsonPatchToXSpecTest { + @Test + public void testEmpty() throws Exception { + // setup + String patchExpression = "[]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder actual = jsonPatch.get(); + // verify + Assert.assertNotNull(actual); + UpdateItemExpressionSpec actualSpec = actual.buildForUpdate(); + Assert.assertNull(actualSpec.getConditionExpression()); + Assert.assertEquals(actualSpec.getUpdateExpression(), ""); + Assert.assertNull(actualSpec.getNameMap()); + Assert.assertNull(actualSpec.getValueMap()); + } + + @Test + public void test_replace_singlePath_number() throws Exception { + // setup + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": 1 } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + UpdateItemExpressionSpec expectedSpec = new ExpressionSpecBuilder() + .addUpdate(ExpressionSpecBuilder.N("a").set(1)) + .withCondition(ExpressionSpecBuilder.attribute_exists("a")) + .buildForUpdate(); + // exercise + ExpressionSpecBuilder actual = jsonPatch.get(); + // verify + Assert.assertNotNull(actual); + UpdateItemExpressionSpec actualSpec = actual.buildForUpdate(); + //the spec builder agressively replaces path components with expression attribute + //with sequentially increasing number strings (#0, #1 etc) + //names in order to avoid name clashes with reserved words/symbols in documents + //"a" was the only path element in the update expression and the only path element + //in the conditions, so it gets the number zero in this example ("attribute_exists(#0)") + Assert.assertEquals(actualSpec.getConditionExpression(), expectedSpec.getConditionExpression()); + Assert.assertEquals(actualSpec.getUpdateExpression(), expectedSpec.getUpdateExpression()); + Assert.assertEquals(actualSpec.getNameMap(), expectedSpec.getNameMap()); + Assert.assertEquals(actualSpec.getValueMap(), expectedSpec.getValueMap()); + } + + @Test + public void test_replace_nestedPath_string() throws Exception { + // setup + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a/b\", \"value\": \"foo\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + UpdateItemExpressionSpec expectedSpec = new ExpressionSpecBuilder() + .addUpdate(ExpressionSpecBuilder.S("a.b").set("foo")) + .withCondition(ExpressionSpecBuilder.attribute_exists("a.b")) + .buildForUpdate(); + // exercise + ExpressionSpecBuilder actual = jsonPatch.get(); + // verify + Assert.assertNotNull(actual); + UpdateItemExpressionSpec actualSpec = actual.buildForUpdate(); + Assert.assertEquals(actualSpec.getConditionExpression(), expectedSpec.getConditionExpression()); + Assert.assertEquals(actualSpec.getUpdateExpression(), expectedSpec.getUpdateExpression()); + Assert.assertEquals(actualSpec.getNameMap(), expectedSpec.getNameMap()); + Assert.assertEquals(actualSpec.getValueMap(), expectedSpec.getValueMap()); + } + + @Test + public void test_remove_singlePath() throws Exception { + // setup + String patchExpression = "[ { \"op\": \"remove\", \"path\": \"/a\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + UpdateItemExpressionSpec expectedSpec = new ExpressionSpecBuilder() + .addUpdate(ExpressionSpecBuilder.NULL("a").remove()) + .buildForUpdate(); + // exercise + ExpressionSpecBuilder actual = jsonPatch.get(); + // verify + Assert.assertNotNull(actual); + UpdateItemExpressionSpec actualSpec = actual.buildForUpdate(); + Assert.assertNull(actualSpec.getConditionExpression()); + Assert.assertEquals(actualSpec.getUpdateExpression(), expectedSpec.getUpdateExpression()); + Assert.assertEquals(actualSpec.getNameMap(), expectedSpec.getNameMap()); + Assert.assertEquals(actualSpec.getValueMap(), expectedSpec.getValueMap()); + } +} diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPathToAttributePathTest.java b/src/test/java/com/github/fge/jsonpatch/JsonPathToAttributePathTest.java new file mode 100644 index 00000000..9e456d8c --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/JsonPathToAttributePathTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016, Alexander Patrikalakis (amcp@me.com) + * Copyright (c) 2015, Daisuke Miyamoto (dai.0304@gmail.com) + * + * This software is dual-licensed under: + * + * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any + * later version; + * - the Apache Software License (ASL) version 2.0. + * + * The text of this file and of both licenses is available at the root of this + * project or, if you have the jar distribution, in directory META-INF/, under + * the names LGPL-3.0.txt and ASL-2.0.txt respectively. + * + * Direct link to the sources: + * + * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt + * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package com.github.fge.jsonpatch; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.github.fge.jackson.jsonpointer.JsonPointer; + +public class JsonPathToAttributePathTest { + + JsonPathToAttributePath sut = new JsonPathToAttributePath(); + + @Test + public void test() throws Exception { + // setup + JsonPointer pointer = new JsonPointer("/a/b/c"); + String expected = "a.b.c"; + // exercise + String actual = sut.apply(pointer); + // verify + Assert.assertEquals(actual, expected); + } + + @Test + public void testFoo() throws Exception { + // setup + JsonPointer pointer = new JsonPointer("/foo"); + String expected = "foo"; + // exercise + String actual = sut.apply(pointer); + // verify + Assert.assertEquals(actual, expected); + } + + @Test + public void testFoo0() throws Exception { + // setup + JsonPointer pointer = new JsonPointer("/foo/0"); + String expected = "foo[0]"; + // exercise + String actual = sut.apply(pointer); + // verify + Assert.assertEquals(actual, expected); + } + + @Test + public void testEmpty() throws Exception { + // setup + JsonPointer pointer = new JsonPointer("/"); + String expected = ""; + // exercise + String actual = sut.apply(pointer); + // verify + Assert.assertEquals(actual, expected); + } + + @Test + public void testSlash() throws Exception { + // setup + JsonPointer pointer = new JsonPointer("/a~1b"); + String expected = "a/b"; + // exercise + String actual = sut.apply(pointer); + // verify + Assert.assertEquals(actual, expected); + } +}