Browse Source

initial commit of function support providing math / JSON helper routines in path execution

pull/103/head
Matt Greenwood 9 years ago
parent
commit
1a0ea4b559
  1. 23
      json-path/src/main/java/com/jayway/jsonpath/AggregationMapReduce.java
  2. 33
      json-path/src/main/java/com/jayway/jsonpath/Function.java
  3. 74
      json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java
  4. 23
      json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java
  5. 18
      json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruFunction.java
  6. 52
      json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java
  7. 26
      json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Average.java
  8. 22
      json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Max.java
  9. 22
      json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java
  10. 24
      json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/StandardDeviation.java
  11. 20
      json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Sum.java
  12. 50
      json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java
  13. 36
      json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java
  14. 23
      json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java
  15. 62
      json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java

23
json-path/src/main/java/com/jayway/jsonpath/AggregationMapReduce.java

@ -0,0 +1,23 @@
package com.jayway.jsonpath;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* Defines a pattern for taking a collection of input streams and executing the same path operation across all files
* map / reducing the results as they come in to provide an aggregation function on top of the
*
* Created by matt@mjgreenwood.net on 6/26/15.
*/
public class AggregationMapReduce {
public static void main(String args[]) throws IOException {
ReadContext ctx = JsonPath.parse(new File("/home/mattg/dev/JsonPath/json-path-assert/src/test/resources/lotto.json"));
List<String> numbers = ctx.read("$.lotto.winners..numbers.%sum()");
Object value = ctx.read("$.lotto.winners.[?(@.winnerId > $.lotto.winners.%length())].numbers.%avg()");
System.out.println(numbers);
System.out.println(value);
}
}

33
json-path/src/main/java/com/jayway/jsonpath/Function.java

@ -0,0 +1,33 @@
package com.jayway.jsonpath;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
/**
* Defines the pattern by which a function can be executed over the result set in the particular path
* being grabbed. The Function's input is the content of the data from the json path selector and its output
* is defined via the functions behavior. Thus transformations in types can take place. Additionally, functions
* can accept multiple selectors in order to produce their output.
*
* Created by matt@mjgreenwood.net on 6/26/15.
*/
public interface Function {
/**
* Invoke the function and output a JSON object (or scalar) value which will be the result of executing the path
*
* @param currentPath
* The current path location inclusive of the function name
* @param parent
* The path location above the current function
*
* @param model
* The JSON model as input to this particular function
*
* @param ctx
* Eval context, state bag used as the path is traversed, maintains the result of executing
*
* @return
*/
Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx);
}

74
json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java

@ -0,0 +1,74 @@
package com.jayway.jsonpath.internal.function;
import com.jayway.jsonpath.Function;
import com.jayway.jsonpath.InvalidPathException;
import com.jayway.jsonpath.internal.function.numeric.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Implements a factory that given a name of the function will return the Function implementation, or null
* if the value is not obtained.
*
* Leverages the function's name in order to determine which function to execute which is maintained internally
* here via a static map
*
* Created by mattg on 6/27/15.
*/
public class FunctionFactory {
public static final Map<String, Class> FUNCTIONS;
static {
// New functions should be added here and ensure the name is not overridden
Map<String, Class> map = new HashMap<String, Class>();
// Math Functions
map.put("avg", Average.class);
map.put("stddev", StandardDeviation.class);
map.put("sum", Sum.class);
map.put("min", Min.class);
map.put("max", Max.class);
// JSON Entity Functions
map.put("length", Length.class);
FUNCTIONS = Collections.unmodifiableMap(map);
}
/**
* Either provides a pass thru function when the function cannot be properly mapped or otherwise returns the function
* implementation based on the name using the internal FUNCTION map
*
* @see #FUNCTIONS
* @see Function
*
* @param pathFragment
* The path fragment that is currently being processed which is believed to be the name of a function
*
* @return
* The implementation of a function
*
* @throws InvalidPathException
*/
public static Function newFunction(String pathFragment) throws InvalidPathException {
Function result = new PassthruFunction();
if (null != pathFragment) {
String name = pathFragment.replaceAll("['%\\]\\[\\(\\)]", "").trim().toLowerCase();
if (null != name && FUNCTIONS.containsKey(name) && Function.class.isAssignableFrom(FUNCTIONS.get(name))) {
try {
result = (Function)FUNCTIONS.get(name).newInstance();
} catch (InstantiationException e) {
throw new InvalidPathException("Function of name: " + name + " cannot be created", e);
} catch (IllegalAccessException e) {
throw new InvalidPathException("Function of name: " + name + " cannot be created", e);
}
}
}
return result;
}
}

23
json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java

@ -0,0 +1,23 @@
package com.jayway.jsonpath.internal.function;
import com.jayway.jsonpath.Function;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
import net.minidev.json.JSONArray;
/**
* Provides the length of a JSONArray Object
*
* Created by mattg on 6/26/15.
*/
public class Length implements Function {
@Override
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) {
if (model instanceof JSONArray) {
JSONArray array = (JSONArray)model;
return Integer.valueOf(array.size());
}
return null;
}
}

18
json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruFunction.java

@ -0,0 +1,18 @@
package com.jayway.jsonpath.internal.function;
import com.jayway.jsonpath.Function;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
/**
* Defines the default behavior which is to return the model that is provided as input as output
*
* Created by mattg on 6/26/15.
*/
public class PassthruFunction implements Function {
@Override
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) {
return model;
}
}

52
json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java

@ -0,0 +1,52 @@
package com.jayway.jsonpath.internal.function.numeric;
import com.jayway.jsonpath.Function;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
import net.minidev.json.JSONArray;
import java.util.Iterator;
/**
* Defines the pattern for processing numerical values via an abstract implementation that iterates over the collection
* of JSONArray entities and verifies that each is a numerical value and then passes that along the abstract methods
*
*
* Created by mattg on 6/26/15.
*/
public abstract class AbstractAggregation implements Function {
/**
* Defines the next value in the array to the mathmatical function
*
* @param value
* The numerical value to process next
*/
protected abstract void next(Number value);
/**
* Obtains the value generated via the series of next value calls
*
* @return
* A numerical answer based on the input value provided
*/
protected abstract Number getValue();
@Override
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) {
if (model instanceof JSONArray) {
JSONArray array = (JSONArray)model;
Double num = 0d;
Iterator<?> it = array.iterator();
while (it.hasNext()) {
Object next = it.next();
if (next instanceof Number) {
Number value = (Number) next;
next(value);
}
}
return getValue();
}
return null;
}
}

26
json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Average.java

@ -0,0 +1,26 @@
package com.jayway.jsonpath.internal.function.numeric;
/**
* Provides the average of a series of numbers in a JSONArray
*
* Created by mattg on 6/26/15.
*/
public class Average extends AbstractAggregation {
private Double summation = 0d;
private Double count = 0d;
@Override
protected void next(Number value) {
count++;
summation += value.doubleValue();
}
@Override
protected Number getValue() {
if (count != 0d) {
return summation / count;
}
return 0d;
}
}

22
json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Max.java

@ -0,0 +1,22 @@
package com.jayway.jsonpath.internal.function.numeric;
/**
* Defines the summation of a series of JSONArray numerical values
*
* Created by mattg on 6/26/15.
*/
public class Max extends AbstractAggregation {
private Double max = Double.MIN_VALUE;
@Override
protected void next(Number value) {
if (max < value.doubleValue()) {
max = value.doubleValue();
}
}
@Override
protected Number getValue() {
return max;
}
}

22
json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java

@ -0,0 +1,22 @@
package com.jayway.jsonpath.internal.function.numeric;
/**
* Defines the summation of a series of JSONArray numerical values
*
* Created by mattg on 6/26/15.
*/
public class Min extends AbstractAggregation {
private Double min = Double.MAX_VALUE;
@Override
protected void next(Number value) {
if (min < value.doubleValue()) {
min = value.doubleValue();
}
}
@Override
protected Number getValue() {
return min;
}
}

24
json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/StandardDeviation.java

@ -0,0 +1,24 @@
package com.jayway.jsonpath.internal.function.numeric;
/**
* Provides the standard deviation of a series of numbers
*
* Created by mattg on 6/27/15.
*/
public class StandardDeviation extends AbstractAggregation {
private Double sumSq = 0d;
private Double sum = 0d;
private Double count = 0d;
@Override
protected void next(Number value) {
sum += value.doubleValue();
sumSq += value.doubleValue()*value.doubleValue();
count++;
}
@Override
protected Number getValue() {
return Math.sqrt(sumSq/count - sum*sum/count/count);
}
}

20
json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Sum.java

@ -0,0 +1,20 @@
package com.jayway.jsonpath.internal.function.numeric;
/**
* Defines the summation of a series of JSONArray numerical values
*
* Created by mattg on 6/26/15.
*/
public class Sum extends AbstractAggregation {
private Double summation = 0d;
@Override
protected void next(Number value) {
summation += value.doubleValue();
}
@Override
protected Number getValue() {
return summation;
}
}

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

@ -0,0 +1,50 @@
package com.jayway.jsonpath.internal.token;
import com.jayway.jsonpath.Function;
import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.internal.function.FunctionFactory;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Token representing a Function call to one of the functions produced via the FunctionFactory
*
* @see FunctionFactory
*
* Created by mattg on 6/27/15.
*/
public class FunctionPathToken extends PathToken {
private final String functionName;
private final String pathFragment;
public FunctionPathToken(String pathFragment) {
this.pathFragment = pathFragment;
Matcher matcher = Pattern.compile(".*?\\%(\\w+)\\(.*?").matcher(pathFragment);
if (matcher.matches()) {
functionName = matcher.group(1);
}
else {
// We'll end up throwing an error from the factory when we get that far
functionName = null;
}
}
@Override
public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) {
Function function = FunctionFactory.newFunction(functionName);
ctx.addResult(currentPath, parent, function.invoke(currentPath, parent, model, ctx));
}
@Override
boolean isTokenDefinite() {
return false;
}
@Override
String getPathFragment() {
return pathFragment;
}
}

36
json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java

@ -0,0 +1,36 @@
package com.jayway.jsonpath.functions;
import com.jayway.jsonpath.Configuration;
import static com.jayway.jsonpath.JsonPath.using;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Created by mattg on 6/27/15.
*/
public class BaseFunctionTest {
protected static final String NUMBER_SERIES = "{\"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}";
protected static final String TEXT_SERIES = "{\"text\" : [ \"a\", \"b\", \"c\", \"d\", \"e\", \"f\" ]}";
/**
* Verify the function returns the correct result based on the input expectedValue
*
* @param pathExpr
* The path expression to execute
*
* @param json
* The json document (actual content) to parse
*
* @param expectedValue
* The expected value to be returned from the test
*/
protected void verifyFunction(String pathExpr, String json, Object expectedValue) {
Configuration conf = Configuration.defaultConfiguration();
assertThat(using(conf).parse(json).read(pathExpr)).isEqualTo(expectedValue);
}
protected void verifyMathFunction(String pathExpr, Object expectedValue) {
verifyFunction(pathExpr, NUMBER_SERIES, expectedValue);
}
}

23
json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java

@ -0,0 +1,23 @@
package com.jayway.jsonpath.functions;
import org.junit.Test;
/**
* Verifies methods that are helper implementations of functions for manipulating JSON entities, i.e.
* length, etc.
*
* Created by mattg on 6/27/15.
*/
public class JSONEntityFunctionTest extends BaseFunctionTest {
@Test
public void testLengthOfTextArray() {
// The length of JSONArray is an integer
System.out.println(TEXT_SERIES);
verifyFunction("$['text'].%length()", TEXT_SERIES, 6);
}
@Test
public void testLengthOfNumberArray() {
// The length of JSONArray is an integer
verifyFunction("$.numbers.%length()", NUMBER_SERIES, 10);
}
}

62
json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java

@ -0,0 +1,62 @@
package com.jayway.jsonpath.functions;
import com.jayway.jsonpath.Configuration;
import net.minidev.json.JSONArray;
import org.junit.Test;
import java.util.Arrays;
import static com.jayway.jsonpath.JsonPath.using;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Defines functional tests around executing:
*
* - sum
* - avg
* - stddev
*
* for each of the above, executes the test and verifies that the results are as expected based on a static input
* and static output.
*
* Created by mattg on 6/26/15.
*/
public class NumericFunctionTest extends BaseFunctionTest {
@Test
public void testAverageOfDoubles() {
verifyMathFunction("$.numbers.%average()", (10d * (10d + 1d)) / 2d);
}
@Test
public void testSumOfDouble() {
verifyMathFunction("$.numbers.%sum()", (10d * (10d + 1d)) / 2d);
}
@Test
public void testMaxOfDouble() {
verifyMathFunction("$.numbers.%max()", 10d);
}
@Test
public void testMinOfDouble() {
verifyMathFunction("$.numbers.%min()", 1d);
}
@Test
public void testStdDevOfDouble() {
verifyMathFunction("$.numbers.%stddev()", 1d);
}
/**
* Expect that for an invalid function name we'll get back the original input to the function
*/
@Test
public void testInvalidFunctionNameNegative() {
Configuration conf = Configuration.defaultConfiguration();
JSONArray numberSeries = new JSONArray();
numberSeries.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
assertThat(using(conf).parse(NUMBER_SERIES).read("$.numbers.%foo()")).isEqualTo(numberSeries);
}
}
Loading…
Cancel
Save