Browse Source

Implement parse() function.

The parse function operates on JSON string properties whose contents are
serialized JSON entities. The function outputs the parsed JSON entity, for
further selection within the serialized object.

For example, given the following JSON:

{ "serialized": "{\"foo\": \"bar\"}" }

The search expression $.serialized.parse().foo would return "bar".
pull/735/head
Drake Tetreault 4 years ago
parent
commit
7e2325dd9f
  1. 23
      README.md
  2. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunctionFactory.java
  3. 27
      json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Parse.java
  4. 7
      json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java
  5. 3
      json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java
  6. 10
      json-path/src/main/java/com/jayway/jsonpath/spi/json/GsonJsonProvider.java
  7. 2
      json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java
  8. 61
      json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java

23
README.md

@ -81,17 +81,18 @@ Functions
Functions can be invoked at the tail end of a path - the input to a function is the output of the path expression. Functions can be invoked at the tail end of a path - the input to a function is the output of the path expression.
The function output is dictated by the function itself. The function output is dictated by the function itself.
| Function | Description | Output type | | Function | Description | Output type |
| :------------------------ | :------------------------------------------------------------------ |:----------- | | :------------------------ | :-------------------------------------------------------------------- |:------------- |
| min() | Provides the min value of an array of numbers | Double | | min() | Provides the min value of an array of numbers | Double |
| max() | Provides the max value of an array of numbers | Double | | max() | Provides the max value of an array of numbers | Double |
| avg() | Provides the average value of an array of numbers | Double | | avg() | Provides the average value of an array of numbers | Double |
| stddev() | Provides the standard deviation value of an array of numbers | Double | | stddev() | Provides the standard deviation value of an array of numbers | Double |
| length() | Provides the length of an array | Integer | | length() | Provides the length of an array | Integer |
| sum() | Provides the sum value of an array of numbers | Double | | sum() | Provides the sum value of an array of numbers | Double |
| keys() | Provides the property keys (An alternative for terminal tilde `~`) | `Set<E>` | | keys() | Provides the property keys (An alternative for terminal tilde `~`) | `Set<E>` |
| concat(X) | Provides a concatinated version of the path output with a new item | like input | | concat(X) | Provides a concatinated version of the path output with a new item | like input |
| append(X) | add an item to the json path output array | like input | | append(X) | add an item to the json path output array | like input |
| parse() | Parse a string property as JSON. Non-string inputs are passed through | Parsed object |
Filter Operators Filter Operators
----------------- -----------------

2
json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunctionFactory.java

@ -10,6 +10,7 @@ import com.jayway.jsonpath.internal.function.numeric.StandardDeviation;
import com.jayway.jsonpath.internal.function.numeric.Sum; import com.jayway.jsonpath.internal.function.numeric.Sum;
import com.jayway.jsonpath.internal.function.text.Concatenate; import com.jayway.jsonpath.internal.function.text.Concatenate;
import com.jayway.jsonpath.internal.function.text.Length; import com.jayway.jsonpath.internal.function.text.Length;
import com.jayway.jsonpath.internal.function.text.Parse;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -40,6 +41,7 @@ public class PathFunctionFactory {
// Text Functions // Text Functions
map.put("concat", Concatenate.class); map.put("concat", Concatenate.class);
map.put("parse", Parse.class);
// JSON Entity Functions // JSON Entity Functions
map.put(Length.TOKEN_NAME, Length.class); map.put(Length.TOKEN_NAME, Length.class);

27
json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Parse.java

@ -0,0 +1,27 @@
package com.jayway.jsonpath.internal.function.text;
import com.jayway.jsonpath.InvalidJsonException;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.internal.function.Parameter;
import com.jayway.jsonpath.internal.function.PathFunction;
import java.util.List;
/**
* Parses a string field containing a serialized JSON object into the object itself.
*/
public class Parse implements PathFunction {
@Override
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx, List<Parameter> parameters) {
final Object unwrapped = ctx.configuration().jsonProvider().unwrap(model);
if (unwrapped instanceof String) {
try {
return ctx.configuration().jsonProvider().parse((String) unwrapped);
} catch (InvalidJsonException e) {
throw new InvalidJsonException(String.format("String property at path %s did not parse as valid JSON", currentPath), e);
}
}
return model;
}
}

7
json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java

@ -38,8 +38,11 @@ public class FunctionPathToken extends PathToken {
PathFunction pathFunction = PathFunctionFactory.newFunction(functionName); PathFunction pathFunction = PathFunctionFactory.newFunction(functionName);
evaluateParameters(currentPath, parent, model, ctx); evaluateParameters(currentPath, parent, model, ctx);
Object result = pathFunction.invoke(currentPath, parent, model, ctx, functionParams); Object result = pathFunction.invoke(currentPath, parent, model, ctx, functionParams);
ctx.addResult(currentPath + "." + functionName, parent, result); // If the function is the leaf token, then its output is emitted as a result. Otherwise, there are still more
if (!isLeaf()) { // tokens to evaluate, so its output is not a result.
if (isLeaf()) {
ctx.addResult(currentPath + "." + functionName, parent, result);
} else {
next().evaluate(currentPath, parent, result, ctx); next().evaluate(currentPath, parent, result, ctx);
} }
} }

3
json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java

@ -235,7 +235,8 @@ public class PathCompiler {
String functionName = path.subSequence(startPosition, endPosition).toString(); String functionName = path.subSequence(startPosition, endPosition).toString();
functionParameters = parseFunctionParameters(functionName); functionParameters = parseFunctionParameters(functionName);
} else { } else {
path.setPosition(readPosition + 1); // Consume the close parenthesis at readPosition + 1 by advancing the position to readPosition + 2.
path.setPosition(readPosition + 2);
} }
} }
else { else {

10
json-path/src/main/java/com/jayway/jsonpath/spi/json/GsonJsonProvider.java

@ -18,8 +18,10 @@ import com.google.gson.Gson;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import com.google.gson.stream.MalformedJsonException;
import com.jayway.jsonpath.InvalidJsonException; import com.jayway.jsonpath.InvalidJsonException;
import com.jayway.jsonpath.JsonPathException; import com.jayway.jsonpath.JsonPathException;
@ -121,7 +123,11 @@ public class GsonJsonProvider extends AbstractJsonProvider {
@Override @Override
public Object parse(final String json) throws InvalidJsonException { public Object parse(final String json) throws InvalidJsonException {
return PARSER.parse(json); try {
return PARSER.parse(json);
} catch (JsonParseException e) {
throw new InvalidJsonException(e);
}
} }
@Override @Override
@ -131,6 +137,8 @@ public class GsonJsonProvider extends AbstractJsonProvider {
return PARSER.parse(new InputStreamReader(jsonStream, charset)); return PARSER.parse(new InputStreamReader(jsonStream, charset));
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new JsonPathException(e); throw new JsonPathException(e);
} catch (JsonParseException e) {
throw new InvalidJsonException(e);
} }
} }

2
json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java

@ -13,7 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
public class BaseFunctionTest { public class BaseFunctionTest {
protected static final String NUMBER_SERIES = "{\"empty\": [], \"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}"; protected static final String NUMBER_SERIES = "{\"empty\": [], \"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}";
protected static final String TEXT_SERIES = "{\"urls\": [\"http://api.worldbank.org/countries/all/?format=json\", \"http://api.worldbank.org/countries/all/?format=json\"], \"text\" : [ \"a\", \"b\", \"c\", \"d\", \"e\", \"f\" ]}"; protected static final String TEXT_SERIES = "{\"urls\": [\"http://api.worldbank.org/countries/all/?format=json\", \"http://api.worldbank.org/countries/all/?format=json\"], \"text\" : [ \"a\", \"b\", \"c\", \"d\", \"e\", \"f\" ], \"parseable\": \"{ \\\"foo\\\": \\\"bar\\\", \\\"array\\\": [1, 2, 4] }\"}";
protected static final String TEXT_AND_NUMBER_SERIES = "{\"text\" : [ \"a\", \"b\", \"c\", \"d\", \"e\", \"f\" ], \"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}"; protected static final String TEXT_AND_NUMBER_SERIES = "{\"text\" : [ \"a\", \"b\", \"c\", \"d\", \"e\", \"f\" ], \"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}";
/** /**

61
json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java

@ -2,12 +2,18 @@ package com.jayway.jsonpath.internal.function;
import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.Configurations; import com.jayway.jsonpath.Configurations;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.InvalidJsonException;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Iterator;
import java.util.List;
import static com.jayway.jsonpath.JsonPath.using; import static com.jayway.jsonpath.JsonPath.using;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -125,4 +131,59 @@ public class NestedFunctionTest extends BaseFunctionTest {
} }
} }
@Test
public void testParseString() {
verifyTextFunction(conf, "$.parseable.parse().foo", "bar");
verifyTextFunction(conf, "$.parseable.parse().array.sum()", 7.0);
}
@Test
public void testIndefiniteParseString() {
String json = "{ \"array\": [ { \"document\": \"{\\\"name\\\": \\\"foo\\\"}\" }, { \"document\": \"{\\\"name\\\": \\\"bar\\\"}\" } ] }";
Object result = using(conf).parse(json).read("$.array[*].document.parse().name");
Assert.assertTrue(conf.jsonProvider().isArray(result));
Iterator<?> resultIter = conf.jsonProvider().toIterable(result).iterator();
Assert.assertTrue(resultIter.hasNext());
Assert.assertEquals("foo", conf.jsonProvider().unwrap(resultIter.next()));
Assert.assertTrue(resultIter.hasNext());
Assert.assertEquals("bar", conf.jsonProvider().unwrap(resultIter.next()));
Assert.assertFalse(resultIter.hasNext());
}
@Test
public void testParseOnNonStrings() {
String json = "{ \"boolean\": true, \"number\": 12.34, \"object\": { \"foo\" : \"bar\" }, \"array\": [ 1, 2 ] }";
DocumentContext doc = using(conf).parse(json);
Assert.assertTrue((boolean) conf.jsonProvider().unwrap(doc.read("$.boolean.parse()")));
Assert.assertEquals(12.34, (double) conf.jsonProvider().unwrap(doc.read("$.number.parse()")), 0.0);
Assert.assertEquals("bar", conf.jsonProvider().unwrap(doc.read("$.object.parse().foo")));
Assert.assertEquals(1, (int) conf.jsonProvider().unwrap(doc.read("$.array.parse()[0]")));
Assert.assertEquals(2, (int) conf.jsonProvider().unwrap(doc.read("$.array.parse()[1]")));
}
@Test
public void testParseOnMalformedJsonString() {
String json = "{\"malformed\": \"{]\"}";
try {
using(conf).parse(json).read("$.malformed.parse()");
Assert.fail("Should have thrown an InvalidJsonException");
} catch (InvalidJsonException e) {
Assert.assertEquals("String property at path $['malformed'] did not parse as valid JSON", e.getMessage());
}
}
@Test
public void testParseOnWellFormedJsonStringsThatAreNotObjects() {
String json = "{\"string\": \"\\\"foo\\\"\", \"number\": \"123\", \"boolean\": \"true\", \"array\": \"[1, 2]\"}";
DocumentContext doc = using(conf).parse(json);
Assert.assertEquals("foo", conf.jsonProvider().unwrap(doc.read("$.string.parse()")));
Assert.assertEquals(123, conf.jsonProvider().unwrap(doc.read("$.number.parse()")));
Assert.assertEquals(true, conf.jsonProvider().unwrap(doc.read("$.boolean.parse()")));
Iterator<?> arrayIter = conf.jsonProvider().toIterable(doc.read("$.array.parse()")).iterator();
Assert.assertTrue(arrayIter.hasNext());
Assert.assertEquals(1, arrayIter.next());
Assert.assertTrue(arrayIter.hasNext());
Assert.assertEquals(2, arrayIter.next());
Assert.assertFalse(arrayIter.hasNext());
}
} }

Loading…
Cancel
Save