Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
dandoug committed Apr 19, 2022
1 parent 7018964 commit ca122aa
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 38 deletions.
70 changes: 70 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '34 20 * * 0'

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
fail-fast: false
matrix:
language: [ 'java' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support

steps:
- name: Checkout repository
uses: actions/checkout@v2

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main

# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1

# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl

# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language

#- run: |
# make bootstrap
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ target/*
.settings/*
.DS_Store
/target/
.idea
2 changes: 1 addition & 1 deletion NOTICE
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
bsonpatch library
Copyright 2017,2018 eBay, Inc.
Copyright 2017,2018,2022 eBay, Inc.

This product includes software developed at
eBay, Inc. (https://www.ebay.com/).
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# This is an implementation of [RFC 6902 JSON Patch](http://tools.ietf.org/html/rfc6902) written in Java.
# This is an implementation of [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) written in Java.

This [JSON Patch](http://jsonpatch.com) implementation works directly with [BSON documents](http://bsonspec.org/) using the [MongoDB Java driver implementation of BSON](https://www.mongodb.com/json-and-bson).

The code here was ported (copied, renamed, repackaged, modified) from the [zjsonpatch project](https://github.com/flipkart-incubator/zjsonpatch).

## Description & Use-Cases
- Java Library to find / apply JSON Patches according to [RFC 6902](http://tools.ietf.org/html/rfc6902).
- Java Library to find / apply JSON Patches according to [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902).
- JSON Patch defines a JSON document structure for representing changes to a JSON document.
- It can be used to avoid sending a whole document when only a part has changed, thus reducing network bandwidth requirements if data (in JSON format) is required to send across multiple systems over network or in case of multi DC transfer.
- This library compares two [BsonValue](http://mongodb.github.io/mongo-java-driver/3.6/javadoc/org/bson/BsonValue.html) inputs and produces a [BsonArray](http://mongodb.github.io/mongo-java-driver/3.6/javadoc/org/bson/BsonArray.html) of the changes.
- When used in combination with the HTTP PATCH method as per [RFC 5789 HTTP PATCH](https://datatracker.ietf.org/doc/html/rfc5789), it will do partial updates for HTTP APIs in a standard way.
- This library compares two [BsonValue](https://mongodb.github.io/mongo-java-driver/3.12/javadoc/org/bson/BsonValue.html) inputs and produces a [BsonArray](https://mongodb.github.io/mongo-java-driver/3.12/javadoc/org/bson/BsonArray.html) of the changes.


### Compatible with : Java 8 and above all versions
Expand All @@ -20,15 +21,15 @@ The code here was ported (copied, renamed, repackaged, modified) from the [zjson

### How to use:

### Current Version : 0.4.9
### Current Version : 0.4.12

Add following to `<dependencies/>` section of your pom.xml -

```xml
<dependency>
<groupId>com.ebay.bsonpatch</groupId>
<artifactId>bsonpatch</artifactId>
<version>0.4.9</version>
<version>0.4.12</version>
</dependency>
```

Expand All @@ -42,7 +43,7 @@ Computes and returns a JSON `patch` (as a BsonArray) from `source` to `target`,
Both `source` and `target` must be either valid BSON objects or arrays or values.
Further, if resultant `patch` is applied to `source`, it will yield `target`.

The algorithm which computes this JsonPatch currently generates following operations as per [RFC 6902](https://tools.ietf.org/html/rfc6902) -
The algorithm which computes this JsonPatch currently generates following operations as per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902#section-4) -
- `add`
- `remove`
- `replace`
Expand Down
16 changes: 8 additions & 8 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.ebay.bsonpatch</groupId>
<artifactId>bsonpatch</artifactId>
<version>0.4.9</version>
<version>0.4.12</version>
<packaging>jar</packaging>

<name>${project.groupId}:${project.artifactId}</name>
Expand Down Expand Up @@ -47,7 +47,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<version>3.10.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
Expand All @@ -61,7 +61,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
Expand Down Expand Up @@ -118,7 +118,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.4</version>
<version>3.3.2</version>
<executions>
<execution>
<id>attach-javadocs</id>
Expand All @@ -137,25 +137,25 @@
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>3.11.2</version>
<version>3.12.10</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.2</version>
<version>4.3</version>
</dependency>
<!-- For IOUtils.toString(inputStream, charset) and StringBuilderWriter -->
<dependency>
<scope>test</scope>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
<version>2.7</version>
</dependency>
<dependency>
<scope>test</scope>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<version>4.13.1</version>
</dependency>
</dependencies>
</project>
51 changes: 40 additions & 11 deletions src/main/java/com/ebay/bsonpatch/BsonDiff.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,29 @@ public static BsonArray asBson(final BsonValue source, final BsonValue target) {

public static BsonArray asBson(final BsonValue source, final BsonValue target, EnumSet<DiffFlags> flags) {
BsonDiff diff = new BsonDiff(flags);

// generating diffs in the order of their occurrence
diff.generateDiffs(JsonPointer.ROOT, source, target);

if (!flags.contains(DiffFlags.OMIT_MOVE_OPERATION)) {
// Merging remove & add to move operation
diff.introduceMoveOperation();
if (source == null && target != null) {
// return add node at root pointing to the target
diff.diffs.add(Diff.generateDiff(Operation.ADD, JsonPointer.ROOT, target));
}

if (!flags.contains(DiffFlags.OMIT_COPY_OPERATION)) {
// Introduce copy operation
diff.introduceCopyOperation(source, target);
if (source != null && target == null) {
// return remove node at root pointing to the source
diff.diffs.add(Diff.generateDiff(Operation.REMOVE, JsonPointer.ROOT, source));
}
if (source != null && target != null) {
diff.generateDiffs(JsonPointer.ROOT, source, target);

if (!flags.contains(DiffFlags.OMIT_MOVE_OPERATION))
// Merging remove & add to move operation
diff.introduceMoveOperation();

if (!flags.contains(DiffFlags.OMIT_COPY_OPERATION))
// Introduce copy operation
diff.introduceCopyOperation(source, target);

if (flags.contains(DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE))
// Split replace into remove and add instructions
diff.introduceExplicitRemoveAndAddOperation();
}
return diff.getBsonNodes();
}

Expand Down Expand Up @@ -212,6 +221,26 @@ private void introduceMoveOperation() {
}
}

/**
* This method splits a {@link Operation#REPLACE} operation within a diff into a {@link Operation#REMOVE}
* and {@link Operation#ADD} in order, respectively.
* Does nothing if {@link Operation#REPLACE} op does not contain a from value
*/
private void introduceExplicitRemoveAndAddOperation() {
List<Diff> updatedDiffs = new ArrayList<Diff>();
for (Diff diff : diffs) {
if (!diff.getOperation().equals(Operation.REPLACE) || diff.getSrcValue() == null) {
updatedDiffs.add(diff);
continue;
}
//Split into two #REMOVE and #ADD
updatedDiffs.add(new Diff(Operation.REMOVE, diff.getPath(), diff.getSrcValue()));
updatedDiffs.add(new Diff(Operation.ADD, diff.getPath(), diff.getValue()));
}
diffs.clear();
diffs.addAll(updatedDiffs);
}

//Note : only to be used for arrays
//Finds the longest common Ancestor ending at Array
private static JsonPointer computeRelativePath(JsonPointer path, int startIdx, int endIdx, List<Diff> diffs) {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/ebay/bsonpatch/CompatibilityFlags.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
public enum CompatibilityFlags {
MISSING_VALUES_AS_NULLS,
REMOVE_NONE_EXISTING_ARRAY_ELEMENT,
ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE;
ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE,
FORBID_REMOVE_MISSING_OBJECT;

public static EnumSet<CompatibilityFlags> defaults() {
return EnumSet.noneOf(CompatibilityFlags.class);
Expand Down
23 changes: 21 additions & 2 deletions src/main/java/com/ebay/bsonpatch/DiffFlags.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.EnumSet;

public enum DiffFlags {

/**
* This flag omits the <i>value</i> field on remove operations.
* This is a default flag.
Expand Down Expand Up @@ -51,14 +52,32 @@ public enum DiffFlags {
* <i>fromValue</i> represents the the value replaced by a {@link Operation#REPLACE}
* operation, in other words, the original value. This can be useful for debugging
* output or custom processing of the diffs by downstream systems.
*
* Please note that this is a non-standard extension to RFC 6902 and will not affect
* how patches produced by this library are processed by this or other libraries.
*
* @since 0.4.1
*/
ADD_ORIGINAL_VALUE_ON_REPLACE,


/**
* This flag normalizes a {@link Operation#REPLACE} operation into its respective
* {@link Operation#REMOVE} and {@link Operation#ADD} operations. Although it adds
* a redundant step, this can be useful for auditing systems in which immutability
* is a requirement.
* <p>
* For the flag to work, {@link DiffFlags#ADD_ORIGINAL_VALUE_ON_REPLACE} has to be
* enabled as the new instructions in the patch need to grab the old <i>fromValue</i>
* {@code "op": "replace", "fromValue": "F1", "value": "F2" }
* The above instruction will be split into
* {@code "op":"remove", "value":"F1" } and {@code "op":"add", "value":"F2"} respectively.
* <p>
* Please note that this is a non-standard extension to RFC 6902 and will not affect
* how patches produced by this library are processed by this or other libraries.
*
* @since 0.4.11
*/
ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE,

/**
* This flag instructs the diff generator to emit {@link Operation#TEST} operations
* that validate the state of the source document before each mutation. This can be
Expand Down
14 changes: 9 additions & 5 deletions src/main/java/com/ebay/bsonpatch/InPlaceApplyProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,17 @@ public void remove(JsonPointer path) throws JsonPointerEvaluationException {

BsonValue parentNode = path.getParent().evaluate(target);
JsonPointer.RefToken token = path.last();
if (parentNode.isDocument())
if (parentNode.isDocument()) {
if (flags.contains(CompatibilityFlags.FORBID_REMOVE_MISSING_OBJECT) && !parentNode.asDocument().containsKey(token.getField()))
throw new BsonPatchApplicationException(
"Missing field " + token.getField(), Operation.REMOVE, path.getParent());
parentNode.asDocument().remove(token.getField());
}
else if (parentNode.isArray()) {
if (!flags.contains(CompatibilityFlags.REMOVE_NONE_EXISTING_ARRAY_ELEMENT) && token.getIndex() >= parentNode.asArray().size()) {
if (!flags.contains(CompatibilityFlags.REMOVE_NONE_EXISTING_ARRAY_ELEMENT) &&
token.getIndex() >= parentNode.asArray().size()) {
throw new BsonPatchApplicationException(
"Array index " + token.getIndex() + " out of bounds", Operation.REPLACE, path.getParent());
"Array index " + token.getIndex() + " out of bounds", Operation.REMOVE, path.getParent());
} else if (token.getIndex() >= parentNode.asArray().size()) {
// do nothing, don't get upset about index out of bounds if REMOVE_NONE_EXISTING_ARRAY_ELEMENT set
// can't just call remove on BsonArray because it throws index out of bounds exception
Expand All @@ -140,7 +144,7 @@ else if (parentNode.isArray()) {
}
} else {
throw new BsonPatchApplicationException(
"Cannot reference past scalar value", Operation.REPLACE, path.getParent());
"Cannot reference past scalar value", Operation.REMOVE, path.getParent());
}
}

Expand Down
19 changes: 18 additions & 1 deletion src/test/java/com/ebay/bsonpatch/CompatibilityTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package com.ebay.bsonpatch;

import static com.ebay.bsonpatch.CompatibilityFlags.ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE;
import static com.ebay.bsonpatch.CompatibilityFlags.FORBID_REMOVE_MISSING_OBJECT;
import static com.ebay.bsonpatch.CompatibilityFlags.MISSING_VALUES_AS_NULLS;
import static com.ebay.bsonpatch.CompatibilityFlags.REMOVE_NONE_EXISTING_ARRAY_ELEMENT;
import static org.hamcrest.core.IsEqual.equalTo;
Expand All @@ -30,6 +31,7 @@

import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonValue;
import org.junit.Before;
import org.junit.Test;

Expand All @@ -39,13 +41,15 @@ public class CompatibilityTest {
BsonArray replaceNodeWithMissingValue;
BsonArray removeNoneExistingArrayElement;
BsonArray replaceNode;
BsonArray removeNode;

@Before
public void setUp() throws Exception {
addNodeWithMissingValue = BsonArray.parse("[{\"op\":\"add\",\"path\":\"/a\"}]");
replaceNodeWithMissingValue = BsonArray.parse("[{\"op\":\"replace\",\"path\":\"/a\"}]");
removeNoneExistingArrayElement = BsonArray.parse("[{\"op\": \"remove\",\"path\": \"/b/0\"}]");
replaceNode = BsonArray.parse("[{\"op\":\"replace\",\"path\":\"/a\",\"value\":true}]");
removeNode = BsonArray.parse("[{\"op\":\"remove\",\"path\":\"/b\"}]");
}

@Test
Expand Down Expand Up @@ -87,4 +91,17 @@ public void withFlagReplaceShouldAddValueWhenMissingInTarget() throws Exception
BsonDocument result = BsonPatch.apply(replaceNode, BsonDocument.parse("{}"), EnumSet.of(ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE)).asDocument();
assertThat(result, equalTo(expected));
}
}

@Test(expected = BsonPatchApplicationException.class)
public void withFlagRemoveMissingValueShouldThrow() throws Exception {
BsonDocument source = BsonDocument.parse("{\"a\": true}");
BsonPatch.apply(removeNode, source, EnumSet.of(FORBID_REMOVE_MISSING_OBJECT));
}

@Test
public void withFlagRemoveShouldRemove() throws Exception {
BsonDocument source = BsonDocument.parse("{\"b\": true}");
BsonDocument expected = BsonDocument.parse("{}");
BsonValue result = BsonPatch.apply(removeNode, source, EnumSet.of(FORBID_REMOVE_MISSING_OBJECT));
assertThat(result, equalTo(expected));
}}
Loading

0 comments on commit ca122aa

Please sign in to comment.