Matt Greenwood
10 years ago
15 changed files with 508 additions and 0 deletions
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
@ -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; |
||||
|
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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…
Reference in new issue