diff --git a/README.md b/README.md index 0cbf944c..cf930fa7 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Its features are: ## Versions -The current version is **1.13**. See file `RELEASE-NOTES.md` for details. +The current version is **1.14**. See file `RELEASE-NOTES.md` for details. ## Using it in your project diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index ed1a2e59..8677ebfd 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,7 @@ +## 1.14 + +* Introduce "remove?" operation + ## 1.13 * Introduce "translate" and "translate?" operations diff --git a/build.gradle b/build.gradle index 31498e58..f6b3f312 100644 --- a/build.gradle +++ b/build.gradle @@ -17,47 +17,47 @@ * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt */ -apply(plugin: "java"); -apply(plugin: "maven"); -apply(plugin: "signing"); -apply(plugin: "osgi"); -apply(plugin: "idea"); -apply(plugin: "eclipse"); - -group = "com.box"; -version = "1.14-SNAPSHOT"; -sourceCompatibility = "1.6"; -targetCompatibility = "1.6"; // defaults to sourceCompatibility +apply(plugin: "java") +apply(plugin: "maven") +apply(plugin: "signing") +apply(plugin: "osgi") +apply(plugin: "idea") +apply(plugin: "eclipse") + +group = "com.box" +version = "1.15-SNAPSHOT" +sourceCompatibility = "1.6" +targetCompatibility = "1.6" // defaults to sourceCompatibility /* * List of dependencies */ dependencies { - compile(group: "com.google.code.findbugs", name: "jsr305", version: "2.0.1"); - compile(group: "com.github.fge", name: "jackson-coreutils", version: "1.6"); + compile(group: "com.google.code.findbugs", name: "jsr305", version: "2.0.1") + compile(group: "com.github.fge", name: "jackson-coreutils", version: "1.6") testCompile(group: "org.testng", name: "testng", version: "6.8.7") { - exclude(group: "junit", module: "junit"); - exclude(group: "org.beanshell", module: "bsh"); - exclude(group: "org.yaml", module: "snakeyaml"); - }; - testCompile(group: "org.mockito", name: "mockito-core", version: "1.9.5"); - testCompile(group: "org.assertj", name: "assertj-core", version: "1.7.0"); + exclude(group: "junit", module: "junit") + exclude(group: "org.beanshell", module: "bsh") + exclude(group: "org.yaml", module: "snakeyaml") + } + testCompile(group: "org.mockito", name: "mockito-core", version: "1.9.5") + testCompile(group: "org.assertj", name: "assertj-core", version: "1.7.0") } -javadoc.options.links("http://docs.oracle.com/javase/6/docs/api/"); -javadoc.options.links("http://jsr-305.googlecode.com/svn/trunk/javadoc/"); -javadoc.options.links("http://fasterxml.github.com/jackson-databind/javadoc/2.2.0/"); -javadoc.options.links("http://fasterxml.github.com/jackson-core/javadoc/2.2.0/"); -javadoc.options.links("http://fasterxml.github.com/jackson-annotations/javadoc/2.2.0/"); -javadoc.options.links("http://docs.guava-libraries.googlecode.com/git-history/v16.0.1/javadoc/"); -javadoc.options.links("http://fge.github.io/msg-simple/"); -javadoc.options.links("http://fge.github.io/jackson-coreutils/"); +javadoc.options.links("http://docs.oracle.com/javase/6/docs/api/") +javadoc.options.links("http://jsr-305.googlecode.com/svn/trunk/javadoc/") +javadoc.options.links("http://fasterxml.github.com/jackson-databind/javadoc/2.2.0/") +javadoc.options.links("http://fasterxml.github.com/jackson-core/javadoc/2.2.0/") +javadoc.options.links("http://fasterxml.github.com/jackson-annotations/javadoc/2.2.0/") +javadoc.options.links("http://docs.guava-libraries.googlecode.com/git-history/v16.0.1/javadoc/") +javadoc.options.links("http://fge.github.io/msg-simple/") +javadoc.options.links("http://fge.github.io/jackson-coreutils/") /* * Repositories to use */ repositories { - mavenCentral(); + mavenCentral() } /* @@ -66,16 +66,16 @@ repositories { */ test { useTestNG() { - useDefaultListeners = true; - }; + useDefaultListeners = true + } } /* * Necessary to generate the source and javadoc jars */ task sourcesJar(type: Jar, dependsOn: classes) { - classifier = "sources"; - from sourceSets.main.allSource; + classifier = "sources" + from sourceSets.main.allSource } /* @@ -88,23 +88,23 @@ task sourcesJar(type: Jar, dependsOn: classes) { //} task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = "javadoc"; - from javadoc.destinationDir; + classifier = "javadoc" + from javadoc.destinationDir } artifacts { - archives jar; - archives sourcesJar; - archives javadocJar; + archives jar + archives sourcesJar + archives javadocJar } task wrapper(type: Wrapper) { - gradleVersion = "2.5"; - distributionUrl = "http://services.gradle.org/distributions/gradle-${gradleVersion}-all.zip"; + gradleVersion = "2.5" + distributionUrl = "http://services.gradle.org/distributions/gradle-${gradleVersion}-all.zip" } task pom << { - pom {}.writeTo("${projectDir}/pom.xml"); + pom {}.writeTo("${projectDir}/pom.xml") } /* @@ -112,47 +112,47 @@ task pom << { */ project.ext { - description = "JSON Patch (RFC 6902) and JSON Merge Patch (RFC 7386) implementation in Java"; - scmURL = sprintf("git@github.com:box-metadata/%s.git", name); - projectURL = sprintf("https://github.com/box-metadata/%s", name); - sonatypeStaging = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"; - sonatypeSnapshots = "https://oss.sonatype.org/content/repositories/snapshots/"; -}; + description = "JSON Patch (RFC 6902) and JSON Merge Patch (RFC 7386) implementation in Java" + scmURL = sprintf("git@github.com:box-metadata/%s.git", name) + projectURL = sprintf("https://github.com/box-metadata/%s", name) + sonatypeStaging = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + sonatypeSnapshots = "https://oss.sonatype.org/content/repositories/snapshots/" +} task checkSigningRequirements << { - def requiredProperties = [ "sonatypeUsername", "sonatypePassword" ]; - def noDice = false; + def requiredProperties = [ "sonatypeUsername", "sonatypePassword" ] + def noDice = false requiredProperties.each { if (project.properties[it] == null) { - noDice = true; + noDice = true System.err.printf("property \"%s\" is not defined!") } } if (noDice) throw new IllegalStateException("missing required properties for " + - "upload"); + "upload") } uploadArchives { - dependsOn(checkSigningRequirements); + dependsOn(checkSigningRequirements) repositories { mavenDeployer { beforeDeployment { - MavenDeployment deployment -> signing.signPom(deployment); + MavenDeployment deployment -> signing.signPom(deployment) } repository(url: "${sonatypeStaging}") { authentication( userName: project.properties["sonatypeUsername"], password: project.properties["sonatypePassword"] - ); + ) } snapshotRepository(url: "${sonatypeSnapshots}") { authentication( userName: project.properties["sonatypeUsername"], password: project.properties["sonatypePassword"] - ); + ) } } } @@ -166,43 +166,43 @@ uploadArchives { uploadArchives.repositories.mavenDeployer ]*.pom*.whenConfigured { pom -> pom.project { - name "${project.name}"; - packaging "jar"; - description "${project.ext.description}"; - url "${projectURL}"; + name "${project.name}" + packaging "jar" + description "${project.ext.description}" + url "${projectURL}" scm { - url "${scmURL}"; - connection "scm:git:${scmURL}"; - developerConnection "scm:git${scmURL}"; + url "${scmURL}" + connection "scm:git:${scmURL}" + developerConnection "scm:git${scmURL}" } licenses { license { - name "Lesser General Public License, version 3 or greater"; - url "http://www.gnu.org/licenses/lgpl.html"; - distribution "repo"; - }; + name "Lesser General Public License, version 3 or greater" + url "http://www.gnu.org/licenses/lgpl.html" + distribution "repo" + } license { - name "Apache Software License, version 2.0"; - url "http://www.apache.org/licenses/LICENSE-2.0"; - distribution "repo"; + name "Apache Software License, version 2.0" + url "http://www.apache.org/licenses/LICENSE-2.0" + distribution "repo" } } developers { developer { - id "metadata-dev"; - name "Metadata Dev"; - email "metadata-dev@box.com"; + id "metadata-dev" + name "Metadata Dev" + email "metadata-dev@box.com" } } } } -ext.forRelease = !version.endsWith("-SNAPSHOT"); +ext.forRelease = !version.endsWith("-SNAPSHOT") signing { - required { forRelease && gradle.taskGraph.hasTask("uploadArchives") }; - sign configurations.archives; + required { forRelease && gradle.taskGraph.hasTask("uploadArchives") } + sign configurations.archives } diff --git a/src/main/java/com/github/fge/jsonpatch/ExtendedJsonPatchFactory.java b/src/main/java/com/github/fge/jsonpatch/ExtendedJsonPatchFactory.java index c8126351..d1c76fe1 100644 --- a/src/main/java/com/github/fge/jsonpatch/ExtendedJsonPatchFactory.java +++ b/src/main/java/com/github/fge/jsonpatch/ExtendedJsonPatchFactory.java @@ -25,6 +25,7 @@ public static JsonPatchFactory create() new NamedType(CopyOperation.class, CopyOperation.OPERATION_NAME), new NamedType(MoveOperation.class, MoveOperation.OPERATION_NAME), new NamedType(RemoveOperation.class, RemoveOperation.OPERATION_NAME), + new NamedType(RemoveOptionalOperation.class, RemoveOptionalOperation.OPERATION_NAME), new NamedType(ReplaceOperation.class, ReplaceOperation.OPERATION_NAME), new NamedType(TestOperation.class, TestOperation.OPERATION_NAME), new NamedType(OmitOperation.class, OmitOperation.OPERATION_NAME), diff --git a/src/main/java/com/github/fge/jsonpatch/JsonPatch.java b/src/main/java/com/github/fge/jsonpatch/JsonPatch.java index 02579798..357a33ac 100644 --- a/src/main/java/com/github/fge/jsonpatch/JsonPatch.java +++ b/src/main/java/com/github/fge/jsonpatch/JsonPatch.java @@ -177,4 +177,8 @@ public void serializeWithType(final JsonGenerator jgen, { serialize(jgen, provider); } + + public List getOperations() { + return ImmutableList.copyOf(operations); + } } diff --git a/src/main/java/com/github/fge/jsonpatch/JsonPatchModule.java b/src/main/java/com/github/fge/jsonpatch/JsonPatchModule.java new file mode 100644 index 00000000..0831a945 --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/JsonPatchModule.java @@ -0,0 +1,31 @@ +package com.github.fge.jsonpatch; + +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.github.fge.jsonpatch.operation.AddOperation; +import com.github.fge.jsonpatch.operation.CopyOperation; +import com.github.fge.jsonpatch.operation.MoveOperation; +import com.github.fge.jsonpatch.operation.RemoveOperation; +import com.github.fge.jsonpatch.operation.ReplaceOperation; +import com.github.fge.jsonpatch.operation.TestOperation; + +/** + * This module registers the standard JSON-PATCH operations with Jackson. + */ +public class JsonPatchModule extends SimpleModule { + private static final long serialVersionUID = 1L; + + /** + * Constructor + */ + public JsonPatchModule() { + registerSubtypes( + new NamedType(AddOperation.class, AddOperation.OPERATION_NAME), + new NamedType(CopyOperation.class, CopyOperation.OPERATION_NAME), + new NamedType(MoveOperation.class, MoveOperation.OPERATION_NAME), + new NamedType(RemoveOperation.class, RemoveOperation.OPERATION_NAME), + new NamedType(ReplaceOperation.class, ReplaceOperation.OPERATION_NAME), + new NamedType(TestOperation.class, TestOperation.OPERATION_NAME) + ); + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/operation/AddOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/AddOperation.java index c5ec79fb..9f7d4ea2 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/AddOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/AddOperation.java @@ -84,14 +84,14 @@ public AddOperation(@JsonProperty("path") final JsonPointer path, public JsonNode apply(final JsonNode node) throws JsonPatchException { - if (path.isEmpty()) + if (getPath().isEmpty()) return value; /* * Check the parent node: it must exist and be a container (ie an array * or an object) for the add operation to work. */ - final JsonNode parentNode = path.parent().path(node); + final JsonNode parentNode = getPath().parent().path(node); if (parentNode.isMissingNode()) throw new JsonPatchException(BUNDLE.getMessage( "jsonPatch.noSuchParent")); @@ -99,8 +99,8 @@ public JsonNode apply(final JsonNode node) throw new JsonPatchException(BUNDLE.getMessage( "jsonPatch.parentNotContainer")); return parentNode.isArray() - ? addToArray(path, node) - : addToObject(path, node); + ? addToArray(getPath(), node) + : addToObject(getPath(), node); } private JsonNode addToArray(final JsonPointer path, final JsonNode node) @@ -119,7 +119,7 @@ private JsonNode addToArray(final JsonPointer path, final JsonNode node) final int index; try { index = Integer.parseInt(token.toString()); - } catch (NumberFormatException ignored) { + } catch (final NumberFormatException ignored) { throw new JsonPatchException(BUNDLE.getMessage( "jsonPatch.notAnIndex")); } diff --git a/src/main/java/com/github/fge/jsonpatch/operation/CopyOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/CopyOperation.java index 87da7d9d..732e357b 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/CopyOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/CopyOperation.java @@ -61,6 +61,6 @@ public JsonNode apply(final JsonNode node) if (dupData.isMissingNode()) throw new JsonPatchException(BUNDLE.getMessage( "jsonPatch.noSuchPath")); - return new AddOperation(path, dupData).apply(node); + return new AddOperation(getPath(), dupData).apply(node); } } diff --git a/src/main/java/com/github/fge/jsonpatch/operation/DualPathOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/DualPathOperation.java index 219e5387..a7cd788e 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/DualPathOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/DualPathOperation.java @@ -35,19 +35,14 @@ /** * Base class for JSON Patch operations taking two JSON Pointers as arguments */ -public abstract class DualPathOperation - implements JsonPatchOperation +public abstract class DualPathOperation extends JsonPatchOperationBase { protected static final MessageBundle BUNDLE = MessageBundles.getBundle(JsonPatchMessages.class); - protected final String op; - @JsonSerialize(using = ToStringSerializer.class) protected final JsonPointer from; - protected final JsonPointer path; - /** * Protected constructor * @@ -58,9 +53,8 @@ public abstract class DualPathOperation protected DualPathOperation(final String op, final JsonPointer from, final JsonPointer path) { - this.op = op; + super(op, path); this.from = from; - this.path = path; } @Override @@ -69,8 +63,8 @@ public final void serialize(final JsonGenerator jgen, throws IOException, JsonProcessingException { jgen.writeStartObject(); - jgen.writeStringField("op", op); - jgen.writeStringField("path", path.toString()); + jgen.writeStringField("op", getOp()); + jgen.writeStringField("path", getPath().toString()); jgen.writeStringField("from", from.toString()); jgen.writeEndObject(); } @@ -86,6 +80,6 @@ public final void serializeWithType(final JsonGenerator jgen, @Override public final String toString() { - return "op: " + op + "; from: \"" + from + "\"; path: \"" + path + '"'; + return "op: " + getOp() + "; from: \"" + from + "\"; path: \"" + getPath() + '"'; } } diff --git a/src/main/java/com/github/fge/jsonpatch/operation/JsonPatchOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/JsonPatchOperation.java index f4063127..d8117cc6 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/JsonPatchOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/JsonPatchOperation.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializable; +import com.github.fge.jackson.jsonpointer.JsonPointer; import com.github.fge.jsonpatch.JsonPatchException; /** @@ -55,6 +56,10 @@ public interface JsonPatchOperation public JsonNode apply(final JsonNode node) throws JsonPatchException; + public String getOp(); + + public JsonPointer getPath(); + @Override public String toString(); } diff --git a/src/main/java/com/github/fge/jsonpatch/operation/JsonPatchOperationBase.java b/src/main/java/com/github/fge/jsonpatch/operation/JsonPatchOperationBase.java new file mode 100644 index 00000000..95b5554d --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/operation/JsonPatchOperationBase.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Jessica Beller (jbeller@box.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.operation; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.github.fge.jackson.jsonpointer.JsonPointer; +import com.github.fge.jsonpatch.operation.policy.PathMissingPolicy; + +/** + * JsonPatchOperationBase implements the basic concept of json patch. + */ +public abstract class JsonPatchOperationBase + implements JsonPatchOperation +{ + private final PathMissingPolicy pathMissingPolicy; + + private final String op; + + @JsonSerialize(using = ToStringSerializer.class) + private final JsonPointer path; + + JsonPatchOperationBase(final String op, final JsonPointer path) + { + this(op, path, null); + } + + JsonPatchOperationBase(final String op, final JsonPointer path, final PathMissingPolicy pathMissingPolicy) + { + this.op = op; + this.path = path; + this.pathMissingPolicy = pathMissingPolicy; + } + + @Override + public String getOp() { return this.op; } + + @Override + public JsonPointer getPath() { return this.path; } + + public PathMissingPolicy getPathMissingPolicy() { return this.pathMissingPolicy; } + + @Override + public String toString() + { + return "op: " + this.op + "; path: \"" + this.path + '"'; + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/operation/MoveOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/MoveOperation.java index 57936831..734e9435 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/MoveOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/MoveOperation.java @@ -79,14 +79,14 @@ public MoveOperation(@JsonProperty("from") final JsonPointer from, public JsonNode apply(final JsonNode node) throws JsonPatchException { - if (from.equals(path)) + if (from.equals(getPath())) return node.deepCopy(); final JsonNode movedNode = from.path(node); if (movedNode.isMissingNode()) throw new JsonPatchException(BUNDLE.getMessage( "jsonPatch.noSuchPath")); final JsonPatchOperation remove = new RemoveOperation(from); - final JsonPatchOperation add = new AddOperation(path, movedNode); + final JsonPatchOperation add = new AddOperation(getPath(), movedNode); return add.apply(remove.apply(node)); } } diff --git a/src/main/java/com/github/fge/jsonpatch/operation/OmitOperationBase.java b/src/main/java/com/github/fge/jsonpatch/operation/OmitOperationBase.java index bdec84e4..506b2036 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/OmitOperationBase.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/OmitOperationBase.java @@ -36,14 +36,14 @@ public JsonNode apply(final JsonNode node) throws JsonPatchException { final JsonNode ret = node.deepCopy(); - if (path.isEmpty()) { + if (getPath().isEmpty()) { if (EQUIVALENCE.equivalent(ret, value)) { return MissingNode.getInstance(); } else { return ret; } } - final JsonNode valueAtPath = path.path(ret); + final JsonNode valueAtPath = getPath().path(ret); if (valueAtPath.isMissingNode()) { switch (pathMissingPolicy) { case THROW: @@ -55,8 +55,8 @@ public JsonNode apply(final JsonNode node) } if (EQUIVALENCE.equivalent(valueAtPath, value)) { - final JsonNode parent = path.parent().get(ret); - final String rawToken = Iterables.getLast(path).getToken().getRaw(); + final JsonNode parent = getPath().parent().get(ret); + final String rawToken = Iterables.getLast(getPath()).getToken().getRaw(); if (parent.isObject()) ((ObjectNode) parent).remove(rawToken); else diff --git a/src/main/java/com/github/fge/jsonpatch/operation/PathDualValueOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/PathDualValueOperation.java index 71f0f70e..0cb4e60f 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/PathDualValueOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/PathDualValueOperation.java @@ -6,7 +6,6 @@ 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.ser.std.ToStringSerializer; import com.github.fge.jackson.jsonpointer.JsonPointer; import com.github.fge.jsonpatch.JsonPatchMessages; import com.github.fge.msgsimple.bundle.MessageBundle; @@ -17,17 +16,11 @@ /** * Base class for JSON Patch operations taking one JSON Pointer and two values as arguments */ -public abstract class PathDualValueOperation - implements JsonPatchOperation +public abstract class PathDualValueOperation extends JsonPatchOperationBase { protected static final MessageBundle BUNDLE = MessageBundles.getBundle(JsonPatchMessages.class); - protected final String op; - - @JsonSerialize(using = ToStringSerializer.class) - protected final JsonPointer path; - @JsonSerialize protected final JsonNode fromValue; @@ -45,8 +38,7 @@ public abstract class PathDualValueOperation protected PathDualValueOperation(final String op, final JsonPointer path, final JsonNode fromValue, final JsonNode toValue) { - this.op = op; - this.path = path; + super(op, path); this.fromValue = fromValue; this.toValue = toValue; } @@ -57,8 +49,8 @@ public final void serialize(final JsonGenerator jgen, throws IOException, JsonProcessingException { jgen.writeStartObject(); - jgen.writeStringField("op", op); - jgen.writeStringField("path", path.toString()); + jgen.writeStringField("op", getOp()); + jgen.writeStringField("path", getPath().toString()); jgen.writeFieldName("from"); jgen.writeTree(fromValue); jgen.writeFieldName("value"); @@ -77,6 +69,6 @@ public final void serializeWithType(final JsonGenerator jgen, @Override public final String toString() { - return "op: " + op + "; path: \"" + path + "\"; from: " + fromValue + "; value: " + toValue; + return "op: " + getOp() + "; path: \"" + getPath() + "\"; from: " + fromValue + "; value: " + toValue; } } diff --git a/src/main/java/com/github/fge/jsonpatch/operation/PathValueOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/PathValueOperation.java index 3b58bf72..13b42faf 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/PathValueOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/PathValueOperation.java @@ -27,7 +27,6 @@ import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.github.fge.jackson.jsonpointer.JsonPointer; import com.github.fge.jsonpatch.JsonPatchMessages; -import com.github.fge.jsonpatch.operation.JsonPatchOperation; import com.github.fge.msgsimple.bundle.MessageBundle; import com.github.fge.msgsimple.load.MessageBundles; @@ -36,16 +35,11 @@ /** * Base class for patch operations taking a value in addition to a path */ -public abstract class PathValueOperation - implements JsonPatchOperation +public abstract class PathValueOperation extends JsonPatchOperationBase { protected static final MessageBundle BUNDLE = MessageBundles.getBundle(JsonPatchMessages.class); - protected final String op; - - protected final JsonPointer path; - @JsonSerialize protected final JsonNode value; @@ -56,11 +50,9 @@ public abstract class PathValueOperation * @param path affected path * @param value JSON value */ - protected PathValueOperation(final String op, final JsonPointer path, - final JsonNode value) + protected PathValueOperation(final String op, final JsonPointer path, final JsonNode value) { - this.op = op; - this.path = path; + super(op, path); this.value = value.deepCopy(); } @@ -70,8 +62,8 @@ public final void serialize(final JsonGenerator jgen, throws IOException, JsonProcessingException { jgen.writeStartObject(); - jgen.writeStringField("op", op); - jgen.writeStringField("path", path.toString()); + jgen.writeStringField("op", getOp()); + jgen.writeStringField("path", getPath().toString()); jgen.writeFieldName("value"); jgen.writeTree(value); jgen.writeEndObject(); @@ -88,6 +80,6 @@ public final void serializeWithType(final JsonGenerator jgen, @Override public final String toString() { - return "op: " + op + "; path: \"" + path + "\"; value: " + value; + return "op: " + getOp() + "; path: \"" + getPath() + "\"; value: " + value; } } diff --git a/src/main/java/com/github/fge/jsonpatch/operation/RemoveOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/RemoveOperation.java index eaf57f7e..40e17fd8 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/RemoveOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/RemoveOperation.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Jessica Beller (jbeller@box.com) * * This software is dual-licensed under: * @@ -21,23 +22,9 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -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.jsontype.TypeSerializer; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.MissingNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.fge.jackson.jsonpointer.JsonPointer; -import com.github.fge.jsonpatch.JsonPatchException; -import com.github.fge.jsonpatch.JsonPatchMessages; -import com.github.fge.jsonpatch.operation.JsonPatchOperation; -import com.github.fge.msgsimple.bundle.MessageBundle; -import com.github.fge.msgsimple.load.MessageBundles; -import com.google.common.collect.Iterables; - -import java.io.IOException; +import com.github.fge.jsonpatch.operation.policy.PathMissingPolicy; /** * JSON Path {@code remove} operation @@ -45,66 +32,14 @@ *

This operation only takes one pointer ({@code path}) as an argument. It * is an error condition if no JSON value exists at that pointer.

*/ -public final class RemoveOperation - implements JsonPatchOperation +public final class RemoveOperation extends RemoveOperationBase { public static final String OPERATION_NAME = "remove"; - protected static final MessageBundle BUNDLE - = MessageBundles.getBundle(JsonPatchMessages.class); - - protected final String op; - - protected final JsonPointer path; - @JsonCreator public RemoveOperation(@JsonProperty("path") final JsonPointer path) { - this.op = OPERATION_NAME; - this.path = path; + super(OPERATION_NAME, path, PathMissingPolicy.THROW); } - @Override - public JsonNode apply(final JsonNode node) - throws JsonPatchException - { - if (path.isEmpty()) - return MissingNode.getInstance(); - if (path.path(node).isMissingNode()) - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.noSuchPath")); - final JsonNode ret = node.deepCopy(); - final JsonNode parentNode = path.parent().get(ret); - final String raw = Iterables.getLast(path).getToken().getRaw(); - if (parentNode.isObject()) - ((ObjectNode) parentNode).remove(raw); - else - ((ArrayNode) parentNode).remove(Integer.parseInt(raw)); - return ret; - } - - @Override - public void serialize(final JsonGenerator jgen, - final SerializerProvider provider) - throws IOException, JsonProcessingException - { - jgen.writeStartObject(); - jgen.writeStringField("op", "remove"); - jgen.writeStringField("path", path.toString()); - jgen.writeEndObject(); - } - - @Override - public void serializeWithType(final JsonGenerator jgen, - final SerializerProvider provider, final TypeSerializer typeSer) - throws IOException, JsonProcessingException - { - serialize(jgen, provider); - } - - @Override - public String toString() - { - return "op: " + op + "; path: \"" + path + '"'; - } } diff --git a/src/main/java/com/github/fge/jsonpatch/operation/RemoveOperationBase.java b/src/main/java/com/github/fge/jsonpatch/operation/RemoveOperationBase.java new file mode 100644 index 00000000..f9f40fae --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/operation/RemoveOperationBase.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * Copyright (c) 2016, Jessica Beller (jbeller@box.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.operation; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.fge.jackson.jsonpointer.JsonPointer; +import com.github.fge.jsonpatch.JsonPatchException; +import com.github.fge.jsonpatch.JsonPatchMessages; +import com.github.fge.jsonpatch.operation.policy.PathMissingPolicy; +import com.github.fge.msgsimple.bundle.MessageBundle; +import com.github.fge.msgsimple.load.MessageBundles; +import com.google.common.collect.Iterables; + +import java.io.IOException; + +/** + * RemoveOperationBase implements the basic concept of removing the requested path. + */ +public abstract class RemoveOperationBase extends JsonPatchOperationBase +{ + protected static final MessageBundle BUNDLE + = MessageBundles.getBundle(JsonPatchMessages.class); + + @JsonCreator + public RemoveOperationBase(final String op, + @JsonProperty("path") final JsonPointer path, + final PathMissingPolicy pathMissingPolicy) + { + super(op, path, pathMissingPolicy); + } + + @Override + public JsonNode apply(final JsonNode node) + throws JsonPatchException + { + final JsonNode ret = node.deepCopy(); + if (this.getPath().isEmpty()) + return MissingNode.getInstance(); + if (this.getPath().path(node).isMissingNode()) { + switch (this.getPathMissingPolicy()) { + case THROW: + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.noSuchPath")); + case SKIP: + return ret; + } + } + final JsonNode parentNode = this.getPath().parent().get(ret); + final String raw = Iterables.getLast(this.getPath()).getToken().getRaw(); + if (parentNode.isObject()) + ((ObjectNode) parentNode).remove(raw); + else + ((ArrayNode) parentNode).remove(Integer.parseInt(raw)); + return ret; + } + + @Override + public void serialize(final JsonGenerator jgen, + final SerializerProvider provider) + throws IOException { + jgen.writeStartObject(); + jgen.writeStringField("op", this.getOp()); + jgen.writeStringField("path", this.getPath().toString()); + jgen.writeEndObject(); + } + + @Override + public void serializeWithType(final JsonGenerator jgen, + final SerializerProvider provider, final TypeSerializer typeSer) + throws IOException { + serialize(jgen, provider); + } + + @Override + public String toString() + { + return "op: " + this.getOp() + "; path: \"" + this.getPath() + '"'; + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/operation/RemoveOptionalOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/RemoveOptionalOperation.java new file mode 100644 index 00000000..7fe0089b --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/operation/RemoveOptionalOperation.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016, Jessica Beller (jbeller@box.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.operation; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jackson.jsonpointer.JsonPointer; +import com.github.fge.jsonpatch.operation.policy.PathMissingPolicy; + +/** + * JSON Path {@code remove?} operation + * + *

This operation will remove ({@code path}) if it exists.

+ */ +public final class RemoveOptionalOperation extends RemoveOperationBase +{ + public static final String OPERATION_NAME = "remove?"; + + @JsonCreator + public RemoveOptionalOperation(@JsonProperty("path") final JsonPointer path) + { + super(OPERATION_NAME, path, PathMissingPolicy.SKIP); + } + +} diff --git a/src/main/java/com/github/fge/jsonpatch/operation/ReplaceOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/ReplaceOperation.java index 916b9c03..dede0e55 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/ReplaceOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/ReplaceOperation.java @@ -26,7 +26,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.fge.jackson.jsonpointer.JsonPointer; import com.github.fge.jsonpatch.JsonPatchException; -import com.github.fge.jsonpatch.operation.PathValueOperation; import com.google.common.collect.Iterables; /** @@ -67,15 +66,15 @@ public JsonNode apply(final JsonNode node) * If remove is done first, the array is empty and add rightly complains * that there is no such index in the array. */ - if (path.path(node).isMissingNode()) + if (getPath().path(node).isMissingNode()) throw new JsonPatchException(BUNDLE.getMessage( "jsonPatch.noSuchPath")); final JsonNode replacement = value.deepCopy(); - if (path.isEmpty()) + if (getPath().isEmpty()) return replacement; final JsonNode ret = node.deepCopy(); - final JsonNode parent = path.parent().get(ret); - final String rawToken = Iterables.getLast(path).getToken().getRaw(); + final JsonNode parent = getPath().parent().get(ret); + final String rawToken = Iterables.getLast(getPath()).getToken().getRaw(); if (parent.isObject()) ((ObjectNode) parent).put(rawToken, replacement); else diff --git a/src/main/java/com/github/fge/jsonpatch/operation/TestOperation.java b/src/main/java/com/github/fge/jsonpatch/operation/TestOperation.java index be324adc..499cfd5e 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/TestOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/TestOperation.java @@ -25,7 +25,6 @@ import com.github.fge.jackson.JsonNumEquals; import com.github.fge.jackson.jsonpointer.JsonPointer; import com.github.fge.jsonpatch.JsonPatchException; -import com.github.fge.jsonpatch.operation.PathValueOperation; import com.google.common.base.Equivalence; /** @@ -60,7 +59,7 @@ public TestOperation(@JsonProperty("path") final JsonPointer path, public JsonNode apply(final JsonNode node) throws JsonPatchException { - final JsonNode tested = path.path(node); + final JsonNode tested = getPath().path(node); if (tested.isMissingNode()) throw new JsonPatchException(BUNDLE.getMessage( "jsonPatch.noSuchPath")); diff --git a/src/main/java/com/github/fge/jsonpatch/operation/TranslateOperationBase.java b/src/main/java/com/github/fge/jsonpatch/operation/TranslateOperationBase.java index 5565a1a6..2ef971bb 100644 --- a/src/main/java/com/github/fge/jsonpatch/operation/TranslateOperationBase.java +++ b/src/main/java/com/github/fge/jsonpatch/operation/TranslateOperationBase.java @@ -38,14 +38,14 @@ public JsonNode apply(final JsonNode node) { final JsonNode ret = node.deepCopy(); final JsonNode toValueRet = toValue.deepCopy(); - if (path.isEmpty()) { + if (getPath().isEmpty()) { if (EQUIVALENCE.equivalent(ret, fromValue)) { return toValueRet; } else { return ret; } } - final JsonNode valueAtPath = path.path(ret); + final JsonNode valueAtPath = getPath().path(ret); if (valueAtPath.isMissingNode()) { switch (pathMissingPolicy) { case THROW: @@ -57,8 +57,8 @@ public JsonNode apply(final JsonNode node) } if (EQUIVALENCE.equivalent(valueAtPath, fromValue)) { - final JsonNode parent = path.parent().get(ret); - final String rawToken = Iterables.getLast(path).getToken().getRaw(); + final JsonNode parent = getPath().parent().get(ret); + final String rawToken = Iterables.getLast(getPath()).getToken().getRaw(); if (parent.isObject()) ((ObjectNode) parent).set(rawToken, toValueRet); else diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchTest.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchTest.java index 6e2af6d3..46c0f341 100644 --- a/src/test/java/com/github/fge/jsonpatch/JsonPatchTest.java +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchTest.java @@ -31,6 +31,7 @@ import org.testng.annotations.Test; import java.io.IOException; +import java.util.List; import static org.mockito.Mockito.*; import static org.testng.Assert.*; @@ -120,4 +121,15 @@ public void whenOneOperationFailsNextOperationIsNotCalled() verifyZeroInteractions(op2); } + + @Test + public void getOperationsReturnsTheCorrectOpsInTheCorrectOrder() + { + final JsonPatch patch = new JsonPatch(ImmutableList.of(op1, op2)); + + List operations = patch.getOperations(); + assertEquals(operations.size(), 2); + assertEquals(operations.get(0), op1); + assertEquals(operations.get(1), op2); + } } diff --git a/src/test/java/com/github/fge/jsonpatch/operation/RemoveOptionalOperationTest.java b/src/test/java/com/github/fge/jsonpatch/operation/RemoveOptionalOperationTest.java new file mode 100644 index 00000000..95b023f7 --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/operation/RemoveOptionalOperationTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016, Jessica Beller (jbeller@box.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.operation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jackson.JacksonUtils; +import com.github.fge.jackson.jsonpointer.JsonPointer; +import com.github.fge.jsonpatch.JsonPatchException; +import org.testng.annotations.Test; + +import java.io.IOException; + +import static org.testng.Assert.*; + +public final class RemoveOptionalOperationTest + extends ExtendedJsonPatchOperationTest +{ + public RemoveOptionalOperationTest() + throws IOException + { + super(RemoveOptionalOperation.OPERATION_NAME); + } + + @Test + public void removingRootReturnsMissingNode() + throws JsonPatchException + { + final JsonNode node = JacksonUtils.nodeFactory().nullNode(); + final JsonPatchOperation op = new RemoveOptionalOperation(JsonPointer.empty()); + final JsonNode ret = op.apply(node); + assertTrue(ret.isMissingNode()); + } +} diff --git a/src/test/java/com/github/fge/jsonpatch/serialization/RemoveOptionalOperationSerializationTest.java b/src/test/java/com/github/fge/jsonpatch/serialization/RemoveOptionalOperationSerializationTest.java new file mode 100644 index 00000000..33307e05 --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/serialization/RemoveOptionalOperationSerializationTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016, Jessica Beller (jbeller@box.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.serialization; + +import com.github.fge.jsonpatch.operation.RemoveOptionalOperation; + +import java.io.IOException; + +public final class RemoveOptionalOperationSerializationTest + extends ExtendedJsonPatchOperationSerializationTest +{ + public RemoveOptionalOperationSerializationTest() + throws IOException + { + super(RemoveOptionalOperation.OPERATION_NAME); + } +} diff --git a/src/test/resources/jsonpatch/extended/remove?.json b/src/test/resources/jsonpatch/extended/remove?.json new file mode 100644 index 00000000..7f7c940e --- /dev/null +++ b/src/test/resources/jsonpatch/extended/remove?.json @@ -0,0 +1,25 @@ +{ + "errors": [], + "ops": [ + { + "op": { "op": "remove?", "path": "/x/y" }, + "node": { "x": { "a": "b", "y": {} } }, + "expected": { "x": { "a": "b" } } + }, + { + "op": { "op": "remove?", "path": "/0/2" }, + "node": [ [ "a", "b", "c"], "d", "e" ], + "expected": [ [ "a", "b" ], "d", "e" ] + }, + { + "op": { "op": "remove?", "path": "/x/0" }, + "node": { "x": [ "y", "z" ], "foo": "bar" }, + "expected": { "x": [ "z" ], "foo": "bar" } + }, + { + "op": { "op": "remove?", "path": "/doesNotExist" }, + "node": { "x": "y" }, + "expected": { "x": "y" } + } + ] +}