Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[US-642] implement simple queries #2

Merged
merged 9 commits into from
Feb 4, 2022
64 changes: 42 additions & 22 deletions src/main/java/com/github/fge/jsonpatch/AddOperation.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.fge.jackson.jsonpointer.JsonPointer;
import com.github.fge.jackson.jsonpointer.ReferenceToken;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;

Expand Down Expand Up @@ -83,23 +79,41 @@ public JsonNode apply(final JsonNode node) throws JsonPatchException {
* Check the parent node: it must exist and be a container (ie an array
* or an object) for the add operation to work.
*/
final int lastSlashIndex = path.lastIndexOf('/');
final String newNodeName = path.substring(lastSlashIndex + 1);
final String pathToParent = path.substring(0, lastSlashIndex);
final String jsonPath = JsonPathParser.tmfStringToJsonPath(pathToParent);
final String fullJsonPath = JsonPathParser.tmfStringToJsonPath(path);
final int lastDotIndex = fullJsonPath.lastIndexOf('.');
final String newNodeName = fullJsonPath.substring(lastDotIndex + 1)
.replace("[", "").replace("]", "");
final String pathToParent = fullJsonPath.substring(0, lastDotIndex);

final DocumentContext nodeContext = JsonPath.parse(node.deepCopy());

final JsonNode parentNode = nodeContext.read(jsonPath);
if (parentNode == null) {
final JsonNode evaluatedJsonParents = nodeContext.read(pathToParent);
if (evaluatedJsonParents == null) {
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchParent"));
}
if (!parentNode.isContainerNode()) {
if (!evaluatedJsonParents.isContainerNode()) {
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.parentNotContainer"));
}

return parentNode.isArray()
? addToArray(nodeContext, jsonPath, newNodeName)
: addToObject(nodeContext, jsonPath, newNodeName);
if (pathToParent.contains("[?(")) { // json filter result is always a list
for (int i = 0; i < evaluatedJsonParents.size(); i++) {
JsonNode parentNode = evaluatedJsonParents.get(i);
if (!parentNode.isContainerNode()) {
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.parentNotContainer"));
}
DocumentContext containerContext = JsonPath.parse(parentNode);
if (parentNode.isArray()) {
addToArray(containerContext, "$", newNodeName);
} else {
addToObject(containerContext, "$", newNodeName);
}
}
return nodeContext.read("$");
} else {
return evaluatedJsonParents.isArray()
? addToArray(nodeContext, pathToParent, newNodeName)
: addToObject(nodeContext, pathToParent, newNodeName);
}
}

private JsonNode addToArray(final DocumentContext node, String jsonPath, String newNodeName) throws JsonPatchException {
Expand All @@ -108,14 +122,7 @@ private JsonNode addToArray(final DocumentContext node, String jsonPath, String
}

final int size = node.read(jsonPath, JsonNode.class).size();
final int index;
try {
index = Integer.parseInt(newNodeName);
} catch (NumberFormatException ignored) {
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.notAnIndex"));
}
if (index < 0 || index > size)
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchIndex"));
final int index = verifyAndGetArrayIndex(newNodeName, size);

ArrayNode updatedArray = node.read(jsonPath, ArrayNode.class).insert(index, value);
return "$".equals(jsonPath) ? updatedArray : node.set(jsonPath, updatedArray).read("$", JsonNode.class);
Expand All @@ -126,4 +133,17 @@ private JsonNode addToObject(final DocumentContext node, String jsonPath, String
.put(jsonPath, newNodeName, value)
.read("$", JsonNode.class);
}

private int verifyAndGetArrayIndex(String stringIndex, int size) throws JsonPatchException {
int index;
try {
index = Integer.parseInt(stringIndex);
} catch (NumberFormatException ignored) {
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.notAnIndex"));
}
if (index < 0 || index > size) {
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchIndex"));
}
return index;
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/github/fge/jsonpatch/DualPathOperation.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ protected DualPathOperation(final String op, final String from, final String pat
public final void serialize(final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("op", op);
jgen.writeStringField("path", path.toString());
jgen.writeStringField("from", from.toString());
jgen.writeStringField("path", path);
jgen.writeStringField("from", from);
jgen.writeEndObject();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
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.msgsimple.bundle.MessageBundle;
import com.github.fge.msgsimple.load.MessageBundles;
import com.jayway.jsonpath.Configuration;
Expand Down
38 changes: 35 additions & 3 deletions src/main/java/com/github/fge/jsonpatch/JsonPathParser.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,49 @@
package com.github.fge.jsonpatch;

import static com.github.fge.jsonpatch.JsonPatchOperation.BUNDLE;

public class JsonPathParser {

private static final String ARRAY_ELEMENT_REGEX = "\\.(\\d+)\\.";
private static final String ARRAY_ELEMENT_LAST_REGEX = "\\.(\\d+)$";

public static String tmfStringToJsonPath(String path) {
public static String tmfStringToJsonPath(String path) throws JsonPatchException {
if (!path.startsWith("/") && !path.isEmpty()) {
return "$." + path;
}
if ("/".equals(path)) {
return "$";
}
final String jsonPath = "$" + path.replace('/', '.')
final String[] pointerAndQuery = path
.replaceAll("(\\w)\\?", "$1#THIS_IS_SPLIT_PLACEHOLDER#")
.split("#THIS_IS_SPLIT_PLACEHOLDER#", -1);
if (pointerAndQuery.length > 2) {
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.invalidPathExpression"));
}

final String jsonPath = "$" + pointerAndQuery[0].replace('/', '.')
.replaceAll(ARRAY_ELEMENT_REGEX, ".[$1].")
.replaceAll(ARRAY_ELEMENT_REGEX, ".[$1].") // has to be repeated due to positive lookahead not working properly
.replaceAll(ARRAY_ELEMENT_LAST_REGEX, ".[$1]");
return jsonPath;
final String jsonPathWithQuery = addQueryIfApplicable(jsonPath, pointerAndQuery);
return jsonPathWithQuery;
}

private static String addQueryIfApplicable(String jsonPath, String[] pointerAndQuery) {
if (pointerAndQuery.length == 2) {
String preparedFilter = pointerAndQuery[1]
.replaceAll("]", "] empty false") // add empty false to nested array expressions
.replaceAll("(\\w)=(\\w)", "$1==$2") // replace single equals with double
.replaceAll("==([\\w .]+)", "=='$1'") // surround strings with single quotes
.replaceFirst("\\w+", "@") // jsonpath expression should start with @ as the name of item
.replaceAll("([&|])\\w+", " $1$1 @"); // replace single | and & with doubles
String filterWithBooleansAndNumbers = preparedFilter
.replaceAll("@([\\w.]+)=='(true|false)'", "(@$1==$2 || @$1=='$2')") // prepare a statement for boolean and boolean as string
.replaceAll("@([\\w.]+)=='(\\d+)'", "(@$1==$2 || @$1=='$2')") // prepare a statement for an integer and integer as string
.replaceAll("@([\\w.]+)=='(\\d+\\.\\d+)'", "(@$1==$2 || @$1=='$2')"); // prepare a statement for float and float as string
return jsonPath.replaceFirst("(\\w+)", "$1[?(" + filterWithBooleansAndNumbers + ")]");
} else {
return jsonPath;
}
}
}
1 change: 0 additions & 1 deletion src/main/java/com/github/fge/jsonpatch/MoveOperation.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
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.jayway.jsonpath.JsonPath;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.github.fge.jackson.jsonpointer.JsonPointer;

import java.io.IOException;

Expand Down
3 changes: 0 additions & 3 deletions src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.fge.jackson.jsonpointer.JsonPointer;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ jsonPatch.noSuchIndex=no such index in target array
jsonPatch.noSuchPath=no such path in target JSON document
jsonPatch.parentNotContainer=parent of path to add to is not a container
jsonPatch.valueTestFailure=value differs from expectations
jsonPatch.invalidPathExpression=invalid path expression
mergePatch.notContainer=value is neither an object or an array (found %s)
81 changes: 81 additions & 0 deletions src/test/java/com/github/fge/jsonpatch/JsonPathParserTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.github.fge.jsonpatch;

import org.testng.annotations.Test;

import static org.testng.Assert.*;

public class JsonPathParserTest {

@Test
public void shouldConvertQueryToJsonPath() throws JsonPatchException {
String jsonPointerWithQuery = "/productPrice/prodPriceAlteration?productPrice.name=Regular Price";
String expected = "$.productPrice[?(@.name=='Regular Price')].prodPriceAlteration";
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
assertEquals(result, expected);
}

@Test
public void shouldConvertArrayPathToJsonPath() throws JsonPatchException {
String jsonPointerWithQuery = "/2/1/-";
String expected = "$.[2].[1].-";
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
assertEquals(result, expected);
}

@Test
public void shouldConvertBooleans() throws JsonPatchException {
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.productOffering.valid=true&orderItem.product.relatedParty.role=customer";
String expected = "$.orderItem[?((@.productOffering.valid==true || @.productOffering.valid=='true') && @.product.relatedParty.role=='customer')].quantity";
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
assertEquals(result, expected);
}

@Test
public void shouldConvertFloatingPoint() throws JsonPatchException {
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.productOffering.price=1513.77&orderItem.product.relatedParty.role=customer";
String expected = "$.orderItem[?((@.productOffering.price==1513.77 || @.productOffering.price=='1513.77') && @.product.relatedParty.role=='customer')].quantity";
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
assertEquals(result, expected);
}

@Test
public void shouldConvertIntegers() throws JsonPatchException {
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.productOffering.id=1513&orderItem.product.relatedParty.role=customer";
String expected = "$.orderItem[?((@.productOffering.id==1513 || @.productOffering.id=='1513') && @.product.relatedParty.role=='customer')].quantity";
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
assertEquals(result, expected);
}

@Test
public void shouldConvertManyConditions() throws JsonPatchException {
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.product.relatedParty.role=customer&orderItem.product.relatedParty.name=Mary";
String expected = "$.orderItem[?(@.product.relatedParty.role=='customer' && @.product.relatedParty.name=='Mary')].quantity";
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
assertEquals(result, expected);
}

@Test
public void shouldConvertNestedArrayQuery() throws JsonPatchException {
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.productOffering.id=1513&orderItem.product.relatedParty[?(@.role=='customer' && @.name=='Mary')]";
String expected = "$.orderItem[?((@.productOffering.id==1513 || @.productOffering.id=='1513') && @.product.relatedParty[?(@.role=='customer' && @.name=='Mary')] empty false)].quantity";
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
assertEquals(result, expected);
}

@Test
public void shouldConvertNestedArrayQueryWhichIsNotLastStatement() throws JsonPatchException {
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.product.relatedParty[?(@.role=='customer' && @.name=='Mary')]&orderItem.productOffering.id=1513";
String expected = "$.orderItem[?(@.product.relatedParty[?(@.role=='customer' && @.name=='Mary')] empty false && (@.productOffering.id==1513 || @.productOffering.id=='1513'))].quantity";
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
assertEquals(result, expected);
}

@Test
public void shouldConvertFilterQuery() throws JsonPatchException {
String filterQuery = "note[?(@.author=='John Doe')].date";
String expected = "$.note[?(@.author=='John Doe')].date";
String result = JsonPathParser.tmfStringToJsonPath(filterQuery);
assertEquals(result, expected);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import com.github.fge.jsonpatch.JsonPatchOperationTest;

import java.io.IOException;
// TODO extend with JsonPatchOperationTest and uncomment constructor when this test needs to be active, couldn't ignore it otherway
public class AddQueryOperationTest extends Object {

public class AddQueryOperationTest extends JsonPatchOperationTest {

public AddQueryOperationTest() throws IOException {
//super("query/add");
super("query/add");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.fge.jsonpatch.query;

import com.github.fge.jsonpatch.JsonPatchOperationTest;

import java.io.IOException;

public class FilterOperationTest extends JsonPatchOperationTest {

public FilterOperationTest() throws IOException {
super("query/filter");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.fge.jsonpatch.query;

import com.github.fge.jsonpatch.JsonPatchOperationTest;

import java.io.IOException;

public class IndividualEntityTest extends JsonPatchOperationTest {

public IndividualEntityTest() throws IOException {
super("query/individual");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

import java.io.IOException;

// TODO extend with JsonPatchOperationTest and uncomment constructor when this test needs to be active, couldn't ignore it otherway
public class RemoveQueryOperationTest extends Object {
public class RemoveQueryOperationTest extends JsonPatchOperationTest {

public RemoveQueryOperationTest() throws IOException {
//super("query/remove");
super("query/remove");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

import java.io.IOException;

// TODO extend with JsonPatchOperationTest and uncomment constructor when this test needs to be active, couldn't ignore it otherway
public class ReplaceQueryOperationTest extends Object {
public class ReplaceQueryOperationTest extends JsonPatchOperationTest {

public ReplaceQueryOperationTest() throws IOException {
//super("query/replace");
super("query/replace");
}
}
Loading