diff --git a/README.md b/README.md index ea1bdd2e..fb828eb7 100644 --- a/README.md +++ b/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. The function output is dictated by the function itself. -| Function | Description | Output type | -| :------------------------ | :------------------------------------------------------------------ |:----------- | -| min() | Provides the min 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 | -| stddev() | Provides the standard deviation value of an array of numbers | Double | -| length() | Provides the length of an array | Integer | -| sum() | Provides the sum value of an array of numbers | Double | -| keys() | Provides the property keys (An alternative for terminal tilde `~`) | `Set` | -| 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 | +| Function | Description | Output type | +| :------------------------ | :-------------------------------------------------------------------- |:------------- | +| min() | Provides the min 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 | +| stddev() | Provides the standard deviation value of an array of numbers | Double | +| length() | Provides the length of an array | Integer | +| sum() | Provides the sum value of an array of numbers | Double | +| keys() | Provides the property keys (An alternative for terminal tilde `~`) | `Set` | +| 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 | +| parse() | Parse a string property as JSON. Non-string inputs are passed through | Parsed object | Filter Operators ----------------- diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunctionFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunctionFactory.java index 2fcd3a33..5da621d2 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunctionFactory.java +++ b/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.text.Concatenate; import com.jayway.jsonpath.internal.function.text.Length; +import com.jayway.jsonpath.internal.function.text.Parse; import java.util.Collections; import java.util.HashMap; @@ -40,6 +41,7 @@ public class PathFunctionFactory { // Text Functions map.put("concat", Concatenate.class); + map.put("parse", Parse.class); // JSON Entity Functions map.put(Length.TOKEN_NAME, Length.class); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Parse.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Parse.java new file mode 100644 index 00000000..091a3aaf --- /dev/null +++ b/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 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; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java index 2d24b70d..9a6c74c3 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java +++ b/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); evaluateParameters(currentPath, parent, model, ctx); Object result = pathFunction.invoke(currentPath, parent, model, ctx, functionParams); - ctx.addResult(currentPath + "." + functionName, parent, result); - if (!isLeaf()) { + // If the function is the leaf token, then its output is emitted as a result. Otherwise, there are still more + // 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); } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java index b7f4a91e..2ad8a082 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java +++ b/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(); functionParameters = parseFunctionParameters(functionName); } else { - path.setPosition(readPosition + 1); + // Consume the close parenthesis at readPosition + 1 by advancing the position to readPosition + 2. + path.setPosition(readPosition + 2); } } else { diff --git a/json-path/src/main/java/com/jayway/jsonpath/spi/json/GsonJsonProvider.java b/json-path/src/main/java/com/jayway/jsonpath/spi/json/GsonJsonProvider.java index d28e7cc7..cd36b090 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/spi/json/GsonJsonProvider.java +++ b/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.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; +import com.google.gson.stream.MalformedJsonException; import com.jayway.jsonpath.InvalidJsonException; import com.jayway.jsonpath.JsonPathException; @@ -121,7 +123,11 @@ public class GsonJsonProvider extends AbstractJsonProvider { @Override 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 @@ -131,6 +137,8 @@ public class GsonJsonProvider extends AbstractJsonProvider { return PARSER.parse(new InputStreamReader(jsonStream, charset)); } catch (UnsupportedEncodingException e) { throw new JsonPathException(e); + } catch (JsonParseException e) { + throw new InvalidJsonException(e); } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java index b26a2b8f..aebd4f07 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java +++ b/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 { 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]}"; /** diff --git a/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java index 2ccc38a7..7054deb9 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java +++ b/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.Configurations; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.InvalidJsonException; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Iterator; +import java.util.List; + import static com.jayway.jsonpath.JsonPath.using; 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()); + } }