com.api.jsonata4java.expressions.ExpressionsVisitor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of JSONata4Java Show documentation
Show all versions of JSONata4Java Show documentation
Port of jsonata.js to Java to enable rules for JSON content
/**
* (c) Copyright 2018, 2019 IBM Corporation
* 1 New Orchard Road,
* Armonk, New York, 10504-1722
* United States
* +1 914 499 1900
* support: Nathaniel Mills [email protected]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.api.jsonata4java.expressions;
import java.math.BigDecimal;
import java.math.MathContext;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.EmptyStackException;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.antlr.v4.runtime.CommonToken;
import org.antlr.v4.runtime.CommonTokenFactory;
import org.antlr.v4.runtime.RuleContext;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeProperty;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.antlr.v4.runtime.tree.TerminalNodeImpl;
import org.apache.commons.text.StringEscapeUtils;
import com.api.jsonata4java.expressions.functions.DeclaredFunction;
import com.api.jsonata4java.expressions.functions.Function;
import com.api.jsonata4java.expressions.generated.MappingExpressionBaseVisitor;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.ArrayContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Array_constructorContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.BooleanContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Context_refContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.ExprContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.ExprListContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.ExprOrSeqContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Fct_chainContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Function_callContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Function_declContext;
// import com.api.jsonata4java.expressions.generated.MappingExpressionParser.FieldListContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.IdContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.NullContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.NumberContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Object_constructorContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.PathContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Root_pathContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.SeqContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.StringContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Var_recallContext;
import com.api.jsonata4java.expressions.utils.BooleanUtils;
import com.api.jsonata4java.expressions.utils.Constants;
import com.api.jsonata4java.expressions.utils.FunctionUtils;
import com.api.jsonata4java.expressions.utils.NumberUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
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.BooleanNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.LongNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
public class ExpressionsVisitor extends MappingExpressionBaseVisitor {
/**
* This is how we indicate to upstream operators that we are currently inside a
* selection statement. E.g. [{"a":1}, {"a":2}}].a will return this special
* subclass of array node. This is so that upstream operators (e.g. array
* indexes) can have special handling applied to them.
*
* For instance: [{"a":1}, {"a":2}}].a[0] does not return [1], but rather [1,2]
*
* Parenthesis can be used to drop this context, e.g.: ([{"a":1},
* {"a":2}}]).a[0] returns [1]
*/
public static class SelectorArrayNode extends ArrayNode {
private List selectionGroups = new ArrayList<>();
public SelectorArrayNode(JsonNodeFactory nc) {
super(nc);
}
/**
* Adds the specified elements to two lists: 1. Adds flattened results to array
* - operators that don't care about selection groups will just see this as a
* normal array
*
* 2. Adds non-flattened results to the separately-maintained selectionGroups
* list. This will be used selector-aware operators (e.g. array index) to apply
* appropriate semantics
*
* @param group
* JsonNode containing the group definitions
*/
public void addAsSelectionGroup(JsonNode group) {
if (group.isArray()) {
this.addAll((ArrayNode) group);
} else {
this.add(group);
}
selectionGroups.add(group);
}
public List getSelectionGroups() {
return Collections.unmodifiableList(this.selectionGroups);
}
}
static private final String _pattern = "#0.##";
static private final DecimalFormat _decimalFormat = new DecimalFormat(_pattern);
static private final String CLASS = ExpressionsVisitor.class.getName();
static public final String ERR_MSG_INVALID_PATH_ENTRY = String.format(Constants.ERR_MSG_INVALID_PATH_ENTRY,
(Object[]) null);
static public final String ERR_NEGATE_NON_NUMERIC = "Cannot negate a non-numeric value";
static public final String ERR_SEQ_LHS_INTEGER = "The left side of the range operator (..) must evaluate to an integer";
static public final String ERR_SEQ_RHS_INTEGER = "The right side of the range operator (..) must evaluate to an integer";
// note: below should read 1e7 not 1e6
static public final String ERR_TOO_BIG = "The size of the sequence allocated by the range operator (..) must not exceed 1e6. Attempted to allocate ";
static private final Logger LOG = Logger.getLogger(CLASS);
static private final Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create();
static private final ObjectMapper objectMapper = new ObjectMapper();
/**
* This defines the behavior of the "=" and "in" operators
*
* @param left
* @param right
* @return
*/
private static boolean areJsonNodesEqual(JsonNode left, JsonNode right) {
if (left.isFloatingPointNumber() || right.isFloatingPointNumber()) {
return (left.asDouble() == right.asDouble());
} else if (left.isIntegralNumber() && right.isIntegralNumber()) {
return left.asLong() == right.asLong();
} else if (left.isNull()) {
return right.isNull();
} else if (right.isNull()) {
return left.isNull();
} else if (left.isTextual() && right.isTextual()) {
return (left.asText().equals(right.asText()));
} else if (left.isArray() && right.isArray()) {
return (left.equals(right));
} else if (left.isObject() && right.isObject()) {
return (left.equals(right));
} else if (left.isBoolean() && right.isBoolean()) {
return left == right;
} else {
// any other permutation of types can never be equal
return false;
}
}
/**
* Encodes JSONata string casting semantics. See
* http://docs.jsonata.org/string-functions.html:
*
* $string(arg)
*
* Casts the arg parameter to a string using the following casting rules
*
* Strings are unchanged Functions are converted to an empty string Numeric
* infinity and NaN throw an error because they cannot be represented as a JSON
* number All other values are converted to a JSON string using the
* JSON.stringify function If arg is not specified (i.e. this function is
* invoked with no arguments), then the context value is used as the value of
* arg.
*
* Examples
*
* $string(5) == "5" [1..5].$string() == ["1", "2", "3", "4", "5"]
*
* @param node
* JsonNode whose content is to be cast as a String
* @param prettify
* Whether the objects or arrays should be pretty printed
* @throws EvaluateRuntimeException
* if json serialization fails
* @return the String representation of the supplied node
*/
public static String castString(JsonNode node, boolean prettify) throws EvaluateRuntimeException {
if (node == null) {
return null;
}
switch (node.getNodeType()) {
case STRING: {
return node.textValue();
}
case NUMBER: {
if (node.isDouble()) {
Double dValue = node.asDouble();
if (Double.isInfinite(dValue) || Double.isNaN(dValue)) {
throw new EvaluateRuntimeException("Attempting to invoke string function on Infinity or NaN");
}
String test = dValue.toString();
// try to determine precision
String strVal = "";
int index = test.indexOf("E");
String expStr = ".e+";
if (index >= 0) {
int minusSize = dValue < 0.0d ? 1 : 0;
int exp = new Integer(test.substring(index + 1));
if (exp < 0) {
expStr = ".e";
}
// how many digits before E?
int len = test.indexOf(".");
if (len - minusSize + Math.abs(exp) <= 21) {
if (exp > 0) {
strVal = _decimalFormat.format(dValue);
return strVal;
}
if (exp > -7) {
strVal = new BigDecimal(dValue, new MathContext(15)).toString().toLowerCase();
if (strVal.indexOf(".") >= 0) {
while (strVal.endsWith("0") && strVal.length() > 0) {
strVal = strVal.substring(0, strVal.length() - 1);
}
}
return strVal;
}
}
// need to check for ".0E" to make it the whole number e+exp
if (test.indexOf(".0E") > 0) {
strVal = test.substring(0, len) + expStr.substring(1) + test.substring(index + 1);
} else {
strVal = test.substring(0, index) + expStr + test.substring(index + 1);
}
} else {
if (prettify) {
strVal = _decimalFormat.format(dValue);
} else {
strVal = new BigDecimal(dValue, new MathContext(15)).toString().toLowerCase();
if (strVal.indexOf(".") >= 0) {
while (strVal.endsWith("0") && strVal.length() > 0) {
strVal = strVal.substring(0, strVal.length() - 1);
}
}
}
}
return strVal;
}
}
default:
// arrays and objects
try {
if (prettify) {
JsonElement jsonElt = JsonParser
.parseString(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(node));
return gson.toJson(jsonElt);
} else {
return objectMapper.writeValueAsString(node);
}
} catch (JsonProcessingException e) {
throw new EvaluateRuntimeException(
"Failed to cast value " + node + " to a string. Reason: " + e.getMessage());
}
}
}
/**
* If input is an array, return it. If input is not an array, wrap it in a
* singleton array and return it.
*
* @param input
* @return wrapped content ensured to be an array
*/
public static ArrayNode ensureArray(JsonNode input) {
if (input == null) {
return null;
} else if (input.isArray()) {
return (ArrayNode) input;
} else {
return JsonNodeFactory.instance.arrayNode().add(input);
}
}
static public ArrayNode flatten(JsonNode arg, ArrayNode flattened) {
if (flattened == null) {
flattened = new ArrayNode(JsonNodeFactory.instance);
}
if (arg.isArray()) {
for (Iterator it = ((ArrayNode) arg).iterator(); it.hasNext();) {
flatten(it.next(), flattened);
}
} else {
flattened.add(arg);
}
return flattened;
}
private static boolean isWholeNumber(double n) {
return n == Math.rint(n) && !Double.isInfinite(n) && !Double.isNaN(n);
}
/**
* Returns true iff the input is already an integral number OR it is a float
* that is exactly equal to an integral number (e.g. 2.0 NOT 2.00001)
*
* Any other type of input will return false
*
* @param n
* @return
*/
private static boolean isWholeNumber(JsonNode n) {
if (n == null) {
return false;
} else if (n.isInt() || n.isLong()) {
return true;
} else if ((n.isFloat() || n.isDouble()) && (n.asInt() == n.asDouble())) {
return true;
} else {
return false;
}
}
private static String sanitise(String str) {
// strip any surrounding quotes
if ((str.startsWith("`") && str.endsWith("`")) || (str.startsWith("\"") && str.endsWith("\""))
|| (str.startsWith("'") && str.endsWith("'"))) {
str = str.substring(1, str.length() - 1);
}
// unescape any special chars
str = StringEscapeUtils.unescapeJson(str);
return str;
}
/**
* If input is a singleton array, return its single element - otherwise return
* the input as is.
*
* @param input
* @return single element if input is a singleton array or the input as
* presented
*/
public static JsonNode unwrapArray(JsonNode input) {
if (input == null) {
return null;
} else if (input.isArray()) {
if (input.size() == 1) {
return input.get(0);
} else {
return input;
}
} else {
return input;
}
}
private FrameEnvironment _environment = null;
private boolean checkRuntime = false;
private int currentDepth = 0;
private JsonNodeFactory factory = JsonNodeFactory.instance;
private boolean firstStep = false;
private boolean firstStepCons = false;
private boolean inArrayConstructor = false;
private boolean keepSingleton = false;
private boolean lastStep = false;
private boolean lastStepCons = false;
private int maxDepth = -1;
private long maxTime = 0L;
private long startTime = new Date().getTime();
private List steps = new ArrayList();
private ParseTreeProperty values = new ParseTreeProperty();
public ExpressionsVisitor() {
setEnvironment(null);
setRootContext(null);
}
public ExpressionsVisitor(JsonNode rootContext, FrameEnvironment environment) throws EvaluateRuntimeException {
setEnvironment(environment);
setRootContext(rootContext);
}
JsonNode append(JsonNode arg1, JsonNode arg2) {
// disregard undefined args
if (arg1 == null) {
return arg2;
}
if (arg2 == null) {
return arg1;
}
// if either argument is not an array, make it so
if (!arg1.isArray()) {
ArrayNode tempArray = new ArrayNode(JsonNodeFactory.instance);
tempArray.add(arg1);
arg1 = tempArray;
}
if (!arg2.isArray()) {
ArrayNode tempArray = new ArrayNode(JsonNodeFactory.instance);
tempArray.add(arg2);
arg2 = tempArray;
}
return ((ArrayNode) arg1).add(((ArrayNode) arg2));
}
private void checkRunaway() {
if (checkRuntime) {
if (maxDepth != -1 && currentDepth > maxDepth) {
throw new EvaluateRuntimeException(
"Stack overflow error: Check for non-terminating recursive function. Consider rewriting as tail-recursive.");
}
}
if (maxTime != 0 && (new Date().getTime() - startTime > maxTime)) {
throw new EvaluateRuntimeException("Expression evaluation timeout: Check for infinite loop");
}
}
private void evaluateEntry() {
currentDepth++;
checkRunaway();
}
private void evaluateExit() {
currentDepth--;
checkRunaway();
}
public Stack getContextStack() {
return _environment.getContextStack();
}
public DeclaredFunction getDeclaredFunction(String fctName) {
return _environment.getDeclaredFunction(fctName);
}
/**
* Grab the current data from the stack and recursively copy its content into an
* array
*
* @return array of the descendant content of the current data on the stack
*/
JsonNode getDescendants() {
JsonNode result = null;
ArrayNode resultArray = new ArrayNode(JsonNodeFactory.instance);
if (_environment.isEmptyContext() == false) {
JsonNode startingElt = _environment.peekContext();
if (startingElt != null) {
traverseDescendants(startingElt, resultArray);
if (resultArray.size() == 1) {
result = resultArray.get(0);
} else {
result = resultArray;
}
}
}
return result;
}
protected FrameEnvironment getEnvironment() {
return _environment;
}
public Function getJsonataFunction(String fctName) {
return _environment.getJsonataFunction(fctName);
}
public int getValue(ParseTree node) {
return values.get(node);
}
public JsonNode getVariable(String varName) {
return _environment.getVariable(varName);
}
boolean isSequence(JsonNode node) {
if (node != null && node instanceof SelectorArrayNode) {
return true;
}
return false;
}
JsonNode lookup(JsonNode input, String key) {
JsonNode result = null;
if (input.isArray()) {
result = new SelectorArrayNode(factory); // factory.arrayNode();
for (int ii = 0; ii < input.size(); ii++) {
JsonNode res = lookup(input.get(ii), key);
if (res != null) {
if (res.isArray()) {
((SelectorArrayNode) result).addAll((ArrayNode) res);
} else {
((SelectorArrayNode) result).add(res);
}
}
}
} else if (input != null && input instanceof ObjectNode) {
result = ((ObjectNode) input).get(key);
}
return result;
}
protected void processArrayContent(ExprOrSeqContext expr, ArrayNode output) {
// have a comma separated list so iterate through the expressions, skipping
// commas
String testStr = "";
for (Iterator it = expr.children.iterator(); it.hasNext();) {
ParseTree tree = it.next();
if (tree == null) {
System.out.println("Got null in array values.");
continue;
}
if (tree instanceof ExprOrSeqContext) {
processArrayContent((ExprOrSeqContext) tree, output);
}
if (tree instanceof TerminalNodeImpl) {
testStr = tree.toString();
if (",[]".indexOf(testStr) >= 0) {
continue;
}
}
JsonNode result = visit(tree);
if (result != null) {
if (inArrayConstructor && result.isArray()) {
output.addAll((ArrayNode) result);
} else {
output.add(result);
}
}
}
}
/**
* Any negative indexes are resolved to start from sizeOfSourceArray. Once
* resolved, the indexes are then sorted into ascending order
*
* @param input
* @param sizeOfSourceArray
* @return
*/
private List resolveIndexes(List input, int sizeOfSourceArray) {
// first we need to resolve negative indexes (they should start from the
// end of this selection group)
List resolvedIndexes = new ArrayList<>();
for (int i : input) {
if (i < 0) {
resolvedIndexes.add(sizeOfSourceArray + i);// remember, index is
// negative
} else {
resolvedIndexes.add(i);
}
}
// now, sort the resolved indexes into ascending order.
// This is required so that the order of the resulting array is the same
// as the order of the source array these indexes are references into.
// e.g. ["a","b","c"][[2,1,0]] -> ["a","b","c"] (not ["c", "b", "a"])
Collections.sort(resolvedIndexes);
return resolvedIndexes;
}
/**
* Given some expression on the lhs of a path, return all values that match the
* rhs of the path. (Pulled out to a separate method so that I can use recursion
* for handling nested arrays - see below)
*
* If lhs=null, return null (e.g. null.a==null)
*
* If lhs is an object, push LHS onto the stack and visit RHS. Ultimately, this
* will result in visitId being called with LHS at the top of the stack. E.g.
* {"a":1}.a==1
*
* if lhs is an array, recurse on each element of the array (e.g. [[{"a":1},
* {"a":2}]].a==[1,2])
*
* @param lhs
* @param rhsCtx
* @return
*/
private JsonNode resolvePath(JsonNode lhs, ExprContext rhsCtx) {
final String METHOD = "resolvePath";
if (LOG.isLoggable(Level.FINEST))
LOG.entering(CLASS, METHOD, new Object[] { lhs, rhsCtx.getText() });
// If the LHS is an array we need to select over all of its elements
// push each onto the context stack and visit the RHS
JsonNode output;
if (lhs == null) {
output = null;
} else if (lhs.isArray()) {
// we need to return this result as a special type of array node
// this is so we know to apply special handling if the (array) result
// of this is indexed
// e.g. [ {"a":[1,2]}, {"a":[3,4]} ].a==[1,2,3,4] (not 1 as one might
// expect)
SelectorArrayNode arr = new SelectorArrayNode(factory);
output = arr;
for (JsonNode lhsE : lhs) {
JsonNode rhsE = resolvePath(lhsE, rhsCtx);
if (rhsE != null) {
arr.addAsSelectionGroup(rhsE);
}
}
// added for Issue#30
// output = unwrapArray(output);
} else {
// the LHS is just an object
_environment.pushContext(lhs);
output = visit(rhsCtx);
_environment.popContext();
}
JsonNode result = output;
if (LOG.isLoggable(Level.FINEST))
LOG.exiting(CLASS, METHOD, result);
return result;
}
public void setDeclaredFunction(String fctName, DeclaredFunction fctValue) {
_environment.setDeclaredFunction(fctName, fctValue);
}
protected void setEnvironment(FrameEnvironment environment) {
if (environment == null) {
environment = new FrameEnvironment(null);
}
_environment = environment;
}
public void setJsonataFunction(String fctName, Function fctValue) {
_environment.setJsonataFunction(fctName, fctValue);
}
protected void setRootContext(JsonNode rootContext) {
this._environment.clearContext();
if (rootContext != null) {
this._environment.pushContext(rootContext);
}
}
public void setValue(ParseTree node, int value) {
values.put(node, value);
}
public void setVariable(String varName, JsonNode varValue) throws EvaluateRuntimeException {
_environment.setVariable(varName, varValue);
}
public void timeboxExpression(long timeoutMS, int maxDepth) {
if (timeoutMS > 0L && maxDepth > 0) {
this.maxDepth = maxDepth;
this.maxTime = timeoutMS;
checkRuntime = true;
}
}
/**
* Recursively traverse the descendants of the input and them in the results
* array
*
* @param input
* the object, array, or node whose descendants are sought
* @param results
* the array containing the descendants of the input and all of
* their descendants
*/
void traverseDescendants(JsonNode input, ArrayNode results) {
if (input != null) {
if (input.isArray() == false) {
// put this element into the results
results.add(input);
}
// now process the "descendants" (array elements, or nodes from keys of an
// object)
if (input.isArray()) {
for (Iterator it = ((ArrayNode) input).iterator(); it.hasNext();) {
JsonNode member = it.next();
traverseDescendants(member, results);
}
} else if (input.isObject()) {
for (Iterator it = ((ObjectNode) input).fieldNames(); it.hasNext();) {
String key = it.next();
traverseDescendants(((ObjectNode) input).get(key), results);
}
}
} // else don't process null
}
@Override
public JsonNode visit(ParseTree tree) {
JsonNode result = null;
if (checkRuntime) {
evaluateEntry();
}
if (steps.size() > 0 && tree.equals(steps.get(steps.size() - 1))) {
lastStep = true;
}
result = super.visit(tree);
if (!keepSingleton) {
if (result != null && result instanceof SelectorArrayNode) {
// && ((SelectorArrayNode) result).getSelectionGroups().size() == 1) {
// result = ((SelectorArrayNode) result).getSelectionGroups().get(0);
if (result.size() == 1) {
result = result.get(0);
}
}
}
if (checkRuntime) {
evaluateExit();
}
return result;
}
@Override
public JsonNode visitAddsub_op(MappingExpressionParser.Addsub_opContext ctx) {
JsonNode leftNode = visit(ctx.expr(0)); // get value of left subexpression
JsonNode rightNode = visit(ctx.expr(1)); // get value of right
// subexpression
// in all cases, if either are *no match*, JSONata returns *no match*
if (leftNode == null || rightNode == null) {
return null;
}
if (!leftNode.isNumber() || !rightNode.isNumber()) {
throw new EvaluateRuntimeException(ctx.op.getText() + " expects two numeric arguments");
}
// treat both inputs as doubles when performing arithmetic operations
double left = leftNode.asDouble();
double right = rightNode.asDouble();
final double result;
if (ctx.op.getType() == MappingExpressionParser.ADD) {
result = left + right;
} else if (ctx.op.getType() == MappingExpressionParser.SUB) {
result = left - right;
} else {
// should never happen (this expression should not have parsed in the
// first place)
throw new EvaluateRuntimeException("Unrecognised token " + ctx.op.getText());
}
// coerce the result to a long iff the result is exactly .0
if (isWholeNumber(result)) {
return new LongNode((long) result);
} else {
return new DoubleNode(result);
}
}
@Override
public JsonNode visitArray(ArrayContext ctx) {
final String METHOD = "visitArray";
if (LOG.isLoggable(Level.FINEST))
LOG.entering(CLASS, METHOD, new Object[] { ctx.getText() });
// we expect exactly two expressions
// expr ARR_START expr ARR_END
// $state.arr[$event.i]
if (ctx.expr().size() != 2) {
// belt&braces - expressions like this should not parse in the first
// place
throw new EvaluateRuntimeException("invalid array expression");
}
// LHS expression (e.g. $event.aaa, [1,2,3,4,5])
// If the LHS is not an array, it will be treated as a singleton array
// e.g. {"a":0}[0] = [{"a":0}][0] = {"a":0}
ArrayNode sourceArray = ensureArray(visit(ctx.expr(0)));
if (sourceArray == null) {
return null;
}
// Expression inside [] (e.g. [1])
ExprContext indexContext = ctx.expr(1);
// this will contain a list of indexes that should be pulled out of the
// source array
// any non-integral array indexes will rounded down in this list
// may contain negative values (these will be normalized later)
final List indexesToReturn = new ArrayList<>();
ArrayNode output = JsonNodeFactory.instance.arrayNode();
boolean isPredicate = false;
// trying changes to emulate jsonata.js behavior at 3:37pm 4/20
// act differently if we have a SelectorArrayNode
boolean sourceIsSAN = false;
List selGroups = new ArrayList();
if (sourceArray instanceof SelectorArrayNode) {
// sourceIsSAN = true;
selGroups = ((SelectorArrayNode) sourceArray).getSelectionGroups();
}
for (int i = 0; i < (sourceIsSAN ? selGroups.size() : sourceArray.size()); i++) {
JsonNode e = (sourceIsSAN ? selGroups.get(i) : sourceArray.get(i));
if (LOG.isLoggable(Level.FINEST))
LOG.logp(Level.FINEST, CLASS, METHOD, "Evaluating array index expression '" + indexContext.getText()
+ "' against element at index " + i + " ('" + e + "') of source array");
// this will cause any expressions to be evaluated with this element of
// the array as context
// in particular, path expressions will resolve relative to the element
// e.g. [{"a":1}, {"a":2}][a=1] - the path expression "a" will resolve
// to 1 (for the first element) and 2 (for the second)
// we need a stack because predicates can be used inside predicates,
// e.g. [{"x":2}, {"x":3}] [x=(["a":101, "b":2}, {"a":102,
// "b":3}][a=101]).b] -> {"x":2}
// check for predicate
_environment.pushContext(e);
JsonNode indexesInContext = factory.arrayNode();
// if (indexContext instanceof Comp_opContext) {
// ArrayNode desiredIndexes = factory.arrayNode();
// isPredicate = true;
// // we can evaluate the comparision by testing its parts
// JsonNode left = visit(((Comp_opContext)indexContext).expr(0)); // get value of left subexpression
// JsonNode right = visit(((Comp_opContext)indexContext).expr(1)); // get value of right subexpression
// if (left!=null) {
// left = ensureArray(left);
// if (right != null) {
// right = ensureArray(right);
// // use the comparator to see which indexes to preserve
// for (int xx = 0; xx [0,1] ... [0,1][""] -> null
// I think we should just throw an exception when the
// expression language
// is used in such an odd way
// if we need strict JSONata compliance in this respect then
// we can change this
if (indexInContext.isBoolean()) {
if (indexInContext.asBoolean()) {
return sourceArray;
} else {
if (indexesInContext.size() == 1) {
return null;
}
return sourceArray; // null;
}
} else {
if (indexInContext.isArray()) {
ArrayNode indexArray = (ArrayNode) indexInContext;
boolean isOkay = false;
for (int j = 0; j < indexArray.size(); j++) {
if (indexArray.get(j).asBoolean()) {
isOkay = true;
break;
}
}
if (isOkay) {
return sourceArray;
} else {
return null;
}
}
throw new NonNumericArrayIndexException();
}
// return sourceArray; // wnm3 added for case002 on multiple-array-selectors
}
}
// because we know it's not a predicate we also know it cannot have
// any path references in it
// this means that it's value is independent of context
// thus we only need to evaluate the index once
break;
}
}
// if (output.size() == 0) {
// now construct the return value based on the indexes computed above
// Is this a (non-predicate) index into the result of a selection? If so,
// the semantics are slightly different to
// an ordinary array index:
//
// [[{"a":1}, {"a":2}, {"a":3}], [{"a":4}, {"a":5}], [{"a":6}]].a[n] ->
// our selection result will look like this:
// [ [1,2,3], [4,5], [6] ]
// ^ group
// n specifies the index (or set of indexes) to pull out from each group,
// e.g.:
// n=0 -> [1,4,6]
// n=1 -> [2,5]
// n=2 -> [3]
// n=[0,1] -> [1,2,4,5,6]
// n=[0,1,2] -> [1,2,3,4,5,6]
// n=[1,2] -> [2,3,5]
// some groups may not be arrays, e.g. [{"a":1}, {"a":2},
// {"a":[3,4]}].a[n] ->
// [1,2,[1,2]]
// n=0 -> [1,2,3]
// n=1 -> 4
// where a group is not an array, treat it as a singleton array
if (!isPredicate && sourceArray instanceof SelectorArrayNode) {
// ^ when predicates are used on selector results, normal array index
// semantics are applied
SelectorArrayNode sourceArraySel = (SelectorArrayNode) sourceArray;
// we need to stay in "selector" mode so that subsequent array index
// references are also treated specially
// e.g. [{"a":1}, {"a":2}}].a[0][0] -> returns [1,2]
SelectorArrayNode resultAsSel = new SelectorArrayNode(factory);
output = resultAsSel;
for (JsonNode group : sourceArraySel.getSelectionGroups()) {
// System.out.println(" group: "+group);
// resolve negative indexes (they should start from the end of this
// selection group)
group = ensureArray(group);
List resolvedIndexes = resolveIndexes(indexesToReturn, group.size());
for (int index : resolvedIndexes) {
// System.out.println(" index: "+index);
if (group.isArray()) {
JsonNode atIndex = group.get(index);
if (atIndex == null) {
// we're done with this group
break;
}
resultAsSel.addAsSelectionGroup(atIndex);
} else {
// treat non-array groups as singleton arrays
if (index == 0) { // non-array groups only have an element at
// index 0
resultAsSel.addAsSelectionGroup(group);
}
}
}
}
} else {
output = factory.arrayNode();
// resolve negative indexes (they should start from the end of the
// source array)
List resolvedIndexes = resolveIndexes(indexesToReturn, sourceArray.size());
// otherwise we just select from the array as normal
for (int index : resolvedIndexes) {
// ignore out-of-bounds indexes
if (index >= 0 && index < sourceArray.size()) {
output.add(sourceArray.get(index));
}
}
}
// }
// results now holds a sub-array of the source array
// containing only those values that are either at the specified indexes
// or match the predicate statement
if (output.size() == 0) {
// if no results are present (i.e. results is empty) we need to return
// null (i.e. *no match*)
return null;
} else {
// if only a single result is present, it should be "unwrapped"
// returned as a non-array
// [1][0]==1
// [[1]][0]==[1]
// [[[1]]][0]==[[1]]
JsonNode result = output;
result = unwrapArray(result);
return result;
}
}
@Override
public JsonNode visitArray_constructor(Array_constructorContext ctx) {
final String METHOD = "visitArray_constructor";
if (LOG.isLoggable(Level.FINEST))
LOG.entering(CLASS, METHOD, new Object[] { ctx.getText(), ctx.depth() });
inArrayConstructor = true;
// System.out.println("========");
// for(ParseTree child : ctx.children) {
// System.out.println(child.getText());
// }
// System.out.println("========");
ArrayNode output = factory.arrayNode();
if (ctx.exprOrSeqList() == null) {
// empty array: []
} else {
for (ExprOrSeqContext expr : ctx.exprOrSeqList().exprOrSeq()) {
if (expr.seq() == null) {
processArrayContent(expr, output);
} else {
// this is a seq, e.g. [1..2]
ArrayNode seq = (ArrayNode) visit(expr);
if (seq == null) {
// in JSONata if either side of the seq is undefined, an empty
// array will be produced
// e.g. [xxx...10]==[]
// in these cases, visitSeq will return null - we just need to
// skip addition to the resulting array in this case
} else {
// we don't want to double-nest the array ([[1,2]]) here so
// addAll, not add()
output.addAll(seq);
}
}
}
inArrayConstructor = false;
}
if (LOG.isLoggable(Level.FINEST))
LOG.exiting(CLASS, METHOD, output.toString());
return output;
}
@Override
public JsonNode visitBoolean(MappingExpressionParser.BooleanContext ctx) {
BooleanNode result = null;
if (ctx.op.getType() == MappingExpressionParser.TRUE) {
result = BooleanNode.TRUE;
} else if (ctx.op.getType() == MappingExpressionParser.FALSE) {
result = BooleanNode.FALSE;
}
return result;
}
// @Override
// public JsonNode visitExpr_to_eof(MappingExpressionParser.Expr_to_eofContext ctx) {
// ParseTree tree = ctx.expr();
// int treeSize = tree.getChildCount();
// steps.clear();
// for (int i = 0; i < treeSize; i++) {
// steps.add(tree.getChild(i));
// }
// if (tree.getChild(0) instanceof Array_constructorContext) {
// firstStepCons = true;
// }
// // test for laststep array construction child[n-1] instanceof
// // Array_constructorContext
// if (tree.getChild(treeSize - 1) instanceof Array_constructorContext) {
// lastStepCons = true;
// }
// if (tree.equals(steps.get(0))) {
// firstStep = true;
// } else {
// firstStep = false;
// }
//
// JsonNode result = visit(tree);
// return result;
// }
@Override
public JsonNode visitComp_op(MappingExpressionParser.Comp_opContext ctx) {
BooleanNode result = null;
JsonNode left = visit(ctx.expr(0)); // get value of left subexpression
JsonNode right = visit(ctx.expr(1)); // get value of right subexpression
// in all cases, if both are *no match*, JSONata returns false
if (left == null && right == null) {
return BooleanNode.FALSE;
}
boolean lIsComparable = false;
boolean rIsComparable = false;
if (left != null && right != null) {
JsonNodeType ltype = left.getNodeType();
JsonNodeType rtype = right.getNodeType();
lIsComparable = ltype == JsonNodeType.NULL || ltype == JsonNodeType.STRING || ltype == JsonNodeType.NUMBER;
rIsComparable = rtype == JsonNodeType.NULL || rtype == JsonNodeType.STRING || rtype == JsonNodeType.NUMBER;
}
// below out for issue #11
// in all cases, if either are *no match*, JSONata returns *no match*
// if (left == null || right == null) {
// return null;
// }
if (ctx.op.getType() == MappingExpressionParser.EQ) {
if (left == null && right != null) {
return BooleanNode.FALSE;
}
if (left != null && right == null) {
return BooleanNode.FALSE;
}
// else both not null
// check if the same types
if (left.getNodeType() == right.getNodeType()) {
return (areJsonNodesEqual(left, right) ? BooleanNode.TRUE : BooleanNode.FALSE);
// return (left.equals(right) ? BooleanNode.TRUE : BooleanNode.FALSE);
}
if (!lIsComparable || !rIsComparable) {
// signal expression
return null;
}
// different types of comparables return not equal
return BooleanNode.FALSE;
} else if (ctx.op.getType() == MappingExpressionParser.NOT_EQ) {
if (left == null && right != null) {
return BooleanNode.TRUE;
}
if (left != null && right == null) {
return BooleanNode.TRUE;
}
// else both not null
// check if the same types
if (left.getNodeType() == right.getNodeType()) {
return (areJsonNodesEqual(left, right) ? BooleanNode.FALSE : BooleanNode.TRUE);
// return (left.equals(right) ? BooleanNode.FALSE : BooleanNode.TRUE);
}
if (!lIsComparable || !rIsComparable) {
// signal expression
return null;
}
// different types of comparables return not equal
return BooleanNode.TRUE;
} else if (ctx.op.getType() == MappingExpressionParser.LT) {
if (left == null || right == null) {
result = null;
} else if (left.isNull() || right.isNull()) {
throw new EvaluateRuntimeException(
"The expressions either side of operator \"<\" must evaluate to numeric or string values");
} else if (left.isBoolean() || right.isBoolean()) {
result = null;
} else if (left.isFloatingPointNumber() || right.isFloatingPointNumber()) {
result = (left.asDouble() < right.asDouble()) ? BooleanNode.TRUE : BooleanNode.FALSE;
} else if (left.isIntegralNumber() && right.isIntegralNumber()) {
result = (left.asLong() < right.asLong()) ? BooleanNode.TRUE : BooleanNode.FALSE;
} else {
if (!lIsComparable || !rIsComparable) {
// signal expression
return null;
}
if (left.getNodeType() != right.getNodeType()) {
throw new EvaluateRuntimeException("The values " + left.toString() + " and " + right.toString()
+ " either side of operator \">\" must be of the same data type");
}
result = (left.asText().compareTo(right.asText()) < 0) ? BooleanNode.TRUE : BooleanNode.FALSE;
}
} else if (ctx.op.getType() == MappingExpressionParser.GT) {
if (left == null || right == null) {
result = null;
} else if (left.isNull() || right.isNull()) {
throw new EvaluateRuntimeException(
"The expressions either side of operator \">\" must evaluate to numeric or string values");
} else if (left.isBoolean() || right.isBoolean()) {
result = null;
} else if (left.isFloatingPointNumber() || right.isFloatingPointNumber()) {
result = (left.asDouble() > right.asDouble()) ? BooleanNode.TRUE : BooleanNode.FALSE;
} else if (left.isIntegralNumber() && right.isIntegralNumber()) {
result = (left.asLong() > right.asLong()) ? BooleanNode.TRUE : BooleanNode.FALSE;
} else {
if (!lIsComparable || !rIsComparable) {
// signal expression
return null;
}
if (left.getNodeType() != right.getNodeType()) {
throw new EvaluateRuntimeException("The values " + left.toString() + " and " + right.toString()
+ " either side of operator \">\" must be of the same data type");
}
result = (left.asText().compareTo(right.asText()) > 0) ? BooleanNode.TRUE : BooleanNode.FALSE;
}
} else if (ctx.op.getType() == MappingExpressionParser.LE) {
if (left == null || right == null) {
result = null;
} else if (left.isNull() || right.isNull()) {
throw new EvaluateRuntimeException(
"The expressions either side of operator \"<=\" must evaluate to numeric or string values");
} else if (left.isBoolean() || right.isBoolean()) {
result = null;
} else if (left.isFloatingPointNumber() || right.isFloatingPointNumber()) {
result = (left.asDouble() <= right.asDouble()) ? BooleanNode.TRUE : BooleanNode.FALSE;
} else if (left.isIntegralNumber() && right.isIntegralNumber()) {
result = (left.asLong() <= right.asLong()) ? BooleanNode.TRUE : BooleanNode.FALSE;
} else {
if (!lIsComparable || !rIsComparable) {
// signal expression
return null;
}
if (left.getNodeType() != right.getNodeType()) {
throw new EvaluateRuntimeException("The values " + left.toString() + " and " + right.toString()
+ " either side of operator \"<=\" must be of the same data type");
}
result = (left.asText().compareTo(right.asText()) <= 0) ? BooleanNode.TRUE : BooleanNode.FALSE;
}
} else if (ctx.op.getType() == MappingExpressionParser.GE) {
if (left == null || right == null) {
result = null;
} else if (left.isNull() || right.isNull()) {
throw new EvaluateRuntimeException(
"The expressions either side of operator \">=\" must evaluate to numeric or string values");
} else if (left.isBoolean() || right.isBoolean()) {
result = null;
} else if (left.isFloatingPointNumber() || right.isFloatingPointNumber()) {
result = (left.asDouble() >= right.asDouble()) ? BooleanNode.TRUE : BooleanNode.FALSE;
} else if (left.isIntegralNumber() && right.isIntegralNumber()) {
result = (left.asLong() >= right.asLong()) ? BooleanNode.TRUE : BooleanNode.FALSE;
} else {
if (!lIsComparable || !rIsComparable) {
// signal expression
return null;
}
if (left.getNodeType() != right.getNodeType()) {
throw new EvaluateRuntimeException("The values " + left.toString() + " and " + right.toString()
+ " either side of operator \">=\" must be of the same data type");
}
result = (left.asText().compareTo(right.asText()) >= 0) ? BooleanNode.TRUE : BooleanNode.FALSE;
}
}
return result;
}
@Override
public JsonNode visitConcat_op(MappingExpressionParser.Concat_opContext ctx) {
JsonNode left = visit(ctx.expr(0)); // get value of left subexpression
JsonNode right = visit(ctx.expr(1)); // get value of right subexpression
String leftStr;
String rightStr;
if (left == null) {
leftStr = "";
} else {
if (left != null && left.isDouble()) {
leftStr = castString(left, false);
} else {
leftStr = castString(left, false);
}
}
if (right == null) {
rightStr = "";
} else {
if (right != null && right.isDouble()) {
rightStr = castString(right, false);
} else {
rightStr = castString(right, false);
}
}
JsonNode result = new TextNode(leftStr + rightStr);
return result;
}
@Override
public JsonNode visitConditional(MappingExpressionParser.ConditionalContext ctx) {
/* the conditional ternary operator ?: */
JsonNode cond = visit(ctx.expr(0)); // get value of left subexpression
if (cond == null) {
ExprContext ctx2 = ctx.expr(2);
if (ctx2 != null) {
return visit(ctx2);
} else {
return null;
}
} else if (cond instanceof BooleanNode) {
ExprContext ctx1 = ctx.expr(1);
ExprContext ctx2 = ctx.expr(2);
if (ctx1 != null && ctx2 != null) {
return BooleanUtils.convertJsonNodeToBoolean(cond) ? visit(ctx.expr(1)) : visit(ctx.expr(2));
} else {
if (BooleanUtils.convertJsonNodeToBoolean(cond)) {
if (ctx1 != null) {
return visit(ctx1);
}
return null;
} else {
if (ctx2 != null) {
return visit(ctx2);
}
return null;
}
}
}
return cond;
}
@Override
public JsonNode visitContext_ref(MappingExpressionParser.Context_refContext ctx) {
JsonNode result = null;
if (ctx.getChildCount() > 0) {
ParseTree child0 = ctx.getChild(0);
if (child0 instanceof TerminalNodeImpl) {
if (((TerminalNodeImpl) child0).symbol.getText().equals("$")) {
// replace first child with value of the rootContext
JsonNode context = getVariable("$");
if (ctx.children.size() == 1) {
// nothing left to visit
return context;
}
/**
* can not just replace ctx child since this can be called recursively with
* different stack values so need to replace child[0] in the new context we
* create below
*/
CommonToken token = null;
ExprContext expr = null;
switch (context.getNodeType()) {
case BINARY:
case POJO: {
break;
}
case ARRAY: {
child0 = FunctionUtils.getArrayConstructorContext(ctx, (ArrayNode) context);
expr = new MappingExpressionParser.ArrayContext(ctx);
for (int i = 0; i < ctx.children.size(); i++) {
expr.children.add(ctx.children.get(i));
}
expr.children.set(0, child0);
result = visit(expr);
break;
}
case BOOLEAN: {
token = (context.asBoolean()
? CommonTokenFactory.DEFAULT.create(MappingExpressionParser.TRUE, context.asText())
: CommonTokenFactory.DEFAULT.create(MappingExpressionParser.FALSE, context.asText()));
TerminalNodeImpl tn = new TerminalNodeImpl(token);
BooleanContext bc = new MappingExpressionParser.BooleanContext(ctx);
bc.children.set(0, tn);
result = visit(bc);
break;
}
case MISSING:
case NULL: {
token = CommonTokenFactory.DEFAULT.create(MappingExpressionParser.NULL, null);
TerminalNodeImpl tn = new TerminalNodeImpl(token);
NullContext nc = new MappingExpressionParser.NullContext(ctx);
nc.children.set(0, tn);
result = visit(nc);
break;
}
case NUMBER: {
token = CommonTokenFactory.DEFAULT.create(MappingExpressionParser.NUMBER, context.asText());
TerminalNodeImpl tn = new TerminalNodeImpl(token);
NumberContext nc = new NumberContext(ctx);
nc.children.set(0, tn);
result = visit(nc);
break;
}
case OBJECT: {
child0 = FunctionUtils.getObjectConstructorContext(ctx, (ObjectNode) context);
expr = new MappingExpressionParser.PathContext(ctx);
for (int i = 0; i < ctx.children.size(); i++) {
expr.children.add(ctx.children.get(i));
}
expr.children.set(0, child0);
result = visit(expr);
break;
}
case STRING:
default: {
token = CommonTokenFactory.DEFAULT.create(MappingExpressionParser.STRING, context.asText());
TerminalNodeImpl tn = new TerminalNodeImpl(token);
StringContext sc = new StringContext(ctx);
sc.children.set(0, tn);
result = visit(sc);
break;
}
} // end switch
// result = visit(expr);
}
} else {
result = visit(child0);
}
}
return result;
}
@Override
public JsonNode visitDescendant(MappingExpressionParser.DescendantContext ctx) {
ArrayNode resultArray = new ArrayNode(JsonNodeFactory.instance);
JsonNode descendants = getDescendants();
ExprContext exprCtx = ctx.expr();
if (descendants == null) {
resultArray = null;
} else {
if (!descendants.isArray()) {
if (exprCtx == null) {
resultArray.add(descendants);
} else {
JsonNode result = visit(exprCtx);
if (result != null) {
resultArray.add(result);
}
}
} else {
for (Iterator it = ((ArrayNode) descendants).iterator(); it.hasNext();) {
if (exprCtx == null) {
resultArray.add(it.next());
} else {
_environment.pushContext(it.next());
JsonNode result = null;
if (exprCtx instanceof MappingExpressionParser.StringContext) {
CommonToken token = CommonTokenFactory.DEFAULT.create(MappingExpressionParser.ID,
visit(exprCtx).asText());
TerminalNode node = new TerminalNodeImpl(token);
IdContext idCtx = new MappingExpressionParser.IdContext(exprCtx);
idCtx.addChild(node);
result = visit(idCtx);
} else {
result = visit(exprCtx);
}
_environment.popContext();
if (result != null) {
resultArray.add(result);
}
}
}
}
}
if (resultArray == null || resultArray.size() == 0) {
return null;
}
JsonNode result = unwrapArray(resultArray);
return result;
}
@Override
public JsonNode visitFct_chain(Fct_chainContext ctx) {
JsonNode result = null;
Object exprObj = ctx.expr(1);
if (exprObj instanceof MappingExpressionParser.Function_callContext) {
JsonNode test = visit(ctx.expr(0));
// leave it to the receiving function to complain
// if (test == null) {
// return null;
// }
_environment.pushContext(test);
result = visit(ctx.expr(1));
_environment.popContext();
} else if (exprObj instanceof Var_recallContext) {
String fctName = ((Var_recallContext) exprObj).VAR_ID().getText();
// assume this is a variable pointing to a function
DeclaredFunction declFct = getDeclaredFunction(fctName);
if (declFct == null) {
Function function = getJsonataFunction(fctName);
if (function == null) {
function = Constants.FUNCTIONS.get(fctName);
}
if (function != null) {
result = function.invoke(this, (Function_callContext) ctx.expr(1));
} else {
throw new EvaluateRuntimeException("Unknown function: " + fctName);
}
} else {
result = declFct.invoke(this, ctx.expr(1));
}
} else
throw new EvaluateRuntimeException("Expected a function but got " + ctx.expr(1).getText());
return result;
}
@Override
public JsonNode visitField_values(MappingExpressionParser.Field_valuesContext ctx) {
ArrayNode resultArray = new ArrayNode(JsonNodeFactory.instance);
ArrayNode valArray = new ArrayNode(JsonNodeFactory.instance);
ExprContext exprCtx = ctx.expr(); // may be null
if (_environment.isEmptyContext()) {
// signal no match
return null;
}
JsonNode elt = _environment.peekContext();
if (elt == null || elt.isObject() == false) {
// signal no match
return null;
}
for (Iterator it = ((ObjectNode) elt).fieldNames(); it.hasNext();) {
JsonNode value = ((ObjectNode) elt).get(it.next());
if (value.isArray()) {
value = flatten(value, null);
// remove outer array
for (Iterator it2 = ((ArrayNode) value).iterator(); it2.hasNext();) {
valArray.add(it2.next());
}
// valArray = (ArrayNode)append(valArray, value);
} else {
valArray.add(value);
}
}
for (Iterator it = valArray.iterator(); it.hasNext();) {
JsonNode value = it.next();
if (exprCtx == null) {
resultArray.add(value);
} else {
_environment.pushContext(value);
JsonNode result = visit(exprCtx);
if (result != null) {
resultArray.add(result);
}
_environment.popContext();
}
}
if (resultArray.size() == 0) {
return null;
}
JsonNode result = unwrapArray(resultArray);
return result;
}
@Override
public JsonNode visitFieldList(MappingExpressionParser.FieldListContext ctx) {
ObjectNode resultObject = new ObjectNode(JsonNodeFactory.instance);
if (_environment.isEmptyContext()) {
// signal no match
return null;
}
JsonNode elt = _environment.peekContext();
if (elt == null || elt.isObject() == false) {
// signal no match
return null;
}
String key = "";
JsonNode value = null;
for (int i = 0; i < ctx.getChildCount(); i += 4) {
// expect name ':' expr ,
key = sanitise(((TerminalNodeImpl) ctx.getChild(i)).getText());
value = visit(ctx.getChild(i + 2));
if (value != null) {
resultObject.set(key, value);
}
}
if (resultObject.size() == 0) {
return null;
}
return resultObject;
}
@Override
public JsonNode visitFunction_call(MappingExpressionParser.Function_callContext ctx) {
JsonNode result = null;
String functionName = ctx.VAR_ID().getText();
DeclaredFunction declFct = getDeclaredFunction(functionName);
if (declFct == null) {
Function function = getJsonataFunction(functionName);
if (function == null) {
function = Constants.FUNCTIONS.get(functionName);
}
if (function != null) {
result = function.invoke(this, ctx);
} else {
throw new EvaluateRuntimeException("Unknown function: " + functionName);
}
} else {
result = declFct.invoke(this, ctx);
}
return result;
}
@Override
public JsonNode visitFunction_decl(MappingExpressionParser.Function_declContext ctx) {
/**
* The goal is to create a JsonNode representing the function declaration that
* could be stored (e.g., keyed by a variable id in the functionMap) and
* executed later by creating a Function_execContext and visiting the
* visitFunciton_exec method.
*/
final String fctName = ctx.FUNCTIONID().getText();
RuleContext expr = ctx.getRuleContext();
JsonNode result = null;
// build the declared function to assign to this variable in the
// functionMap
MappingExpressionParser.Function_declContext fctDeclCtx = (MappingExpressionParser.Function_declContext) expr;
MappingExpressionParser.VarListContext varList = fctDeclCtx.varList();
MappingExpressionParser.ExprListContext exprList = fctDeclCtx.exprList();
DeclaredFunction fct = new DeclaredFunction(varList, exprList);
setDeclaredFunction(fctName, fct);
return result;
}
@Override
public JsonNode visitFunction_exec(MappingExpressionParser.Function_execContext ctx) {
// "function($l, $w, $h){ $l * $w * $h }(10, 10, 5)"
// get the VarListContext for the variables to be assigned
// get the ExprValuesContext for the values to be assigned to the
// variables
// get the ExprListContext to calculate the result
JsonNode result = null;
List varListCtx = ctx.varList().VAR_ID();
List exprValuesCtx = ctx.exprValues().exprList().expr();
int varListCount = varListCtx.size();
int exprListCount = exprValuesCtx.size();
// ensure a direct mapping is possible
if (varListCount != exprListCount) {
throw new EvaluateRuntimeException(
"Expected equal counts for varibles (" + varListCount + ") and values (" + exprListCount + ")");
}
for (int i = 0; i < varListCount; i++) {
String varID = varListCtx.get(i).getText();
JsonNode value = visit(exprValuesCtx.get(i));
setVariable(varID, value);
}
ExprListContext exprListCtx = ctx.exprList();
result = visit(exprListCtx);
return result;
}
@Override
public JsonNode visitId(IdContext ctx) {
final String METHOD = "visitId";
JsonNode context;
try {
context = _environment.peekContext();
} catch (EmptyStackException ex) {
return null;
}
final String id = sanitise(ctx.ID().getText());
if (LOG.isLoggable(Level.FINEST))
LOG.entering(CLASS, METHOD, new Object[] { ctx.getText(), id, "(stack: " + context + ")" });
JsonNode result = null;
if (context == null) {
result = null;
} else {
result = lookup(context, id);
}
if (LOG.isLoggable(Level.FINEST))
LOG.exiting(CLASS, METHOD, result);
return result;
}
// private boolean flattenOutput = true;
@Override
public JsonNode visitLogand(MappingExpressionParser.LogandContext ctx) {
BooleanNode result = null;
JsonNode left = visit(ctx.expr(0)); // get value of left subexpression
JsonNode right = visit(ctx.expr(1)); // get value of right subexpression
// if (left == null || right == null)
// return null;
result = BooleanUtils.convertJsonNodeToBoolean(left) && BooleanUtils.convertJsonNodeToBoolean(right)
? BooleanNode.TRUE
: BooleanNode.FALSE;
return result;
}
@Override
public JsonNode visitLogor(MappingExpressionParser.LogorContext ctx) {
BooleanNode result = null;
JsonNode left = visit(ctx.expr(0)); // get value of left subexpression
JsonNode right = visit(ctx.expr(1)); // get value of right subexpression
// if (left == null || right == null)
// return null;
result = BooleanUtils.convertJsonNodeToBoolean(left) || BooleanUtils.convertJsonNodeToBoolean(right)
? BooleanNode.TRUE
: BooleanNode.FALSE;
return result;
}
@Override
public JsonNode visitMembership(MappingExpressionParser.MembershipContext ctx) {
final String METHOD = "visitMembership";
JsonNode left = visit(ctx.expr(0)); // get value of left subexpression
JsonNode right = visit(ctx.expr(1)); // get value of right subexpression
if (left == null || right == null)
return null;
// If the RHS is a single value, then it is treated as a singleton array.
right = ensureArray(right);
// unwrap left if singleton
left = unwrapArray(left);
BooleanNode result = BooleanNode.FALSE;
Iterator elements = right.elements();
while (elements.hasNext()) {
JsonNode curElement = elements.next();
if (areJsonNodesEqual(left, curElement)) {
result = BooleanNode.TRUE;
break;
}
}
if (LOG.isLoggable(Level.FINEST))
LOG.logp(Level.FINEST, CLASS, METHOD, "is " + left + " (" + left.getNodeType() + ") in " + right + " ("
+ right.getNodeType() + ")? -> " + result);
return result;
}
@Override
public JsonNode visitMuldiv_op(MappingExpressionParser.Muldiv_opContext ctx) {
JsonNode leftNode = visit(ctx.expr(0)); // get value of left subexpression
JsonNode rightNode = visit(ctx.expr(1)); // get value of right
// subexpression
// in all cases, if either are *no match*, JSONata returns *no match*
if (leftNode == null || rightNode == null) {
return null;
}
if (!leftNode.isNumber() || !rightNode.isNumber()) {
throw new EvaluateRuntimeException(ctx.op.getText() + " expects two numeric arguments");
}
// treat both inputs as doubles when performing arithmetic operations
double left = leftNode.asDouble();
double right = rightNode.asDouble();
final Double result;
if (ctx.op.getType() == MappingExpressionParser.MUL) {
result = left * right;
} else if (ctx.op.getType() == MappingExpressionParser.DIV) {
if (right == 0.0d) {
return new DoubleNode(Double.POSITIVE_INFINITY); // null;
}
result = left / right;
} else if (ctx.op.getType() == MappingExpressionParser.REM) {
if (right == 0.0d) {
return new DoubleNode(Double.POSITIVE_INFINITY); // null;
}
result = left % right;
} else {
// should never happen (this expression should not have parsed in the
// first place)
throw new EvaluateRuntimeException("Unrecognised token " + ctx.op.getText());
}
// check for Infinity and Nan
if (result.isInfinite() || result.isNaN()) {
throw new EvaluateRuntimeException("Number out of range: \"null\"");
}
// coerce the result to a long iff the result is exactly .0
if (isWholeNumber(result)) {
return new LongNode(result.longValue());
} else {
return new DoubleNode(result);
}
}
@Override
public JsonNode visitNull(NullContext ctx) {
return NullNode.getInstance();
}
@Override
public JsonNode visitNumber(MappingExpressionParser.NumberContext ctx) {
/*
* For consistency with the JavaScript implementation of JSONata, we limit the
* size of the numbers that we handle to be within the range Double.MAX_VALUE
* and -Double.MAX_VALUE. If we did not do this we would need to implement a lot
* of extra code to handle BigInteger and BigDecimal. The
* NumberUtils::convertNumberToValueNode will check whether the number is within
* the valid range and throw a suitable exception if it is not.
*/
return NumberUtils.convertNumberToValueNode(ctx.NUMBER().getText());
}
@Override
public JsonNode visitObject_constructor(Object_constructorContext ctx) {
ObjectNode object = factory.objectNode();
// convert the keys to strings, stripping surrounding quotes and
// unescaping any special JSON chars
if (ctx.fieldList() == null) { // (ctx.expr() == null) {
// empty object: {}
return object;
} else {
// FieldListContext fieldList = (FieldListContext)ctx.expr();
// List keys = fieldList.STRING().stream().map(k -> k.getText()).map(k
// -> sanitise(k))
// List keys = ctx.fieldList().STRING().stream().map(k -> k.getText()).map(k -> sanitise(k))
// .collect(Collectors.toList());
List keyNodes = ctx.fieldList().STRING();
List valueNodes = ctx.fieldList().expr();
if (keyNodes.size() != valueNodes.size()) {
// this shouldn't have parsed in the first place
throw new EvaluateRuntimeException("Object key/value count mismatch!");
}
ExprContext valueNode = null;
JsonNode value = null;
String fctName = null;
String key = "";
for (int i = 0; i < keyNodes.size(); i++) {
key = keyNodes.get(i).getText();
key = sanitise(key);
// try to get the value for the corresponding expression
valueNode = valueNodes.get(i);
if (valueNode instanceof Function_declContext) {
visit(valueNode);
fctName = ((Function_declContext) valueNode).FUNCTIONID().getText();
if (getDeclaredFunction(fctName) != null) {
value = new TextNode("");
}
} else if (valueNode instanceof Var_recallContext) {
value = visit(valueNode);
if (value == null) {
String varName = ((Var_recallContext) valueNode).VAR_ID().getText();
DeclaredFunction declFct = getDeclaredFunction(varName);
if (declFct != null) {
value = new TextNode("");
} else {
Function fct = getJsonataFunction(varName);
if (fct != null) {
value = new TextNode("");
} else {
value = null;
}
}
}
} else {
value = visit(valueNode);
// check for double infinity
if (value != null && value.isDouble()) {
Double d = value.asDouble();
if (Double.isNaN(d) || Double.isInfinite(d)) {
throw new EvaluateRuntimeException("Number out of range: null");
}
}
}
if (value != null) {
object.set(key, value);
}
}
// List values = fieldList.expr().stream().map(e ->
// visit(e)).collect(Collectors.toList());
// List values = ctx.fieldList().expr().stream().map(e -> visit(e)).collect(Collectors.toList());
// if (keys.size() != values.size()) {
// // this shouldn't have parsed in the first place
// throw new EvaluateRuntimeException("Object key/value count mismatch!");
// }
//
// for (int i = 0; i < keys.size(); i++) {
// String key = keys.get(i);
// JsonNode value = values.get(i);
// if (value != null) {
// object.set(key, value);
// }
// }
return object;
}
}
@Override
public JsonNode visitParens(MappingExpressionParser.ParensContext ctx) {
JsonNode result = null;
// generate a new frameEnvironment for life of this block
FrameEnvironment oldEnvironment = _environment;
_environment = new FrameEnvironment(_environment);
List expressions = ctx.expr();
for (int i = 0; i < expressions.size(); i++) {
result = visit(ctx.expr(i));
}
// result = visit(ctx.expr(1));
// JsonNode result = visit(ctx.expr());
// we need to drop out of selection mode if the params wrap a selection
// statement
// this is so that e.g. ([{"a":1}, {"a":2}].a)[0] returns 1 (not [1,2] as
// would be returned without the parenthesis)
if (result instanceof SelectorArrayNode) {
ArrayNode newResult = factory.arrayNode();
((ArrayNode) newResult).addAll((SelectorArrayNode) result);
result = newResult;
}
_environment = oldEnvironment;
return result;
}
@Override
public JsonNode visitPath(PathContext ctx) {
final String METHOD = "visitPath";
if (LOG.isLoggable(Level.FINEST))
LOG.entering(CLASS, METHOD, ctx.getText());
// [{"a":{"b":1}}, {"a":{"b":2}}].a
final ExprContext lhsCtx = ctx.expr(0); // e.g. [{"a":{"b":1}},
// {"a":{"b":2}}]
final ExprContext rhsCtx = ctx.expr(1); // e.g. `a`
// treat a StringContext as an ID context
JsonNode lhs = null;
if (lhsCtx instanceof MappingExpressionParser.StringContext) {
CommonToken token = CommonTokenFactory.DEFAULT.create(MappingExpressionParser.ID, visit(lhsCtx).asText());
TerminalNode node = new TerminalNodeImpl(token);
IdContext idCtx = new MappingExpressionParser.IdContext(lhsCtx);
idCtx.addChild(node);
lhs = visit(idCtx);
} else {
lhs = visit(lhsCtx);
}
if (lhs == null || lhs.isNull()) {
return null;
// throw new EvaluateRuntimeException(String.format(Constants.ERR_MSG_INVALID_PATH_ENTRY,"null"));
}
// reject path entries that are numbers or values
switch (lhs.getNodeType()) {
case NUMBER: {
lhs = factory.textNode(lhs.asText());
break;
}
case BOOLEAN:
case NULL: {
throw new EvaluateRuntimeException(String.format(Constants.ERR_MSG_INVALID_PATH_ENTRY, lhs.toString()));
}
default: {
break;
}
}
JsonNode result;
if (rhsCtx == null) {
result = lhs;
} else {
JsonNode rhs = null;
// treat a StringContext as an ID Context
if (rhsCtx instanceof MappingExpressionParser.StringContext) {
CommonToken token = CommonTokenFactory.DEFAULT.create(MappingExpressionParser.ID, visit(rhsCtx).asText());
TerminalNode node = new TerminalNodeImpl(token);
IdContext idCtx = new MappingExpressionParser.IdContext(rhsCtx);
idCtx.addChild(node);
rhs = resolvePath(lhs, idCtx);
} else if (rhsCtx instanceof NumberContext) {
throw new EvaluateRuntimeException(
"The literal value " + visit(rhsCtx) + " cannot be used as a step within a path expression");
// } else if (rhsCtx instanceof FieldListContext) {
// _environment.pushContext(lhs);
// rhs = visit(rhsCtx);
// _environment.popContext();
} else {
rhs = resolvePath(lhs, rhsCtx);
}
if (rhs == null) { // okay to return NullNode here so don't test "|| rhs.isNull()"
result = null;
} else if (rhs instanceof SelectorArrayNode) {
if (rhs.size() == 0) {
// if no results are present (i.e. results is empty) we need to return
// null (i.e. *no match*)
return null;
} else {
if ((firstStepCons && firstStep) || (lastStep && lastStepCons)) {
List cells = ((SelectorArrayNode) rhs).getSelectionGroups();
result = new ArrayNode(JsonNodeFactory.instance);
for (JsonNode cell : cells) {
((ArrayNode) result).add(cell);
}
firstStepCons = false;
} else {
result = rhs;
}
}
} else {
result = rhs;
}
}
if (LOG.isLoggable(Level.FINEST))
LOG.exiting(CLASS, METHOD, result);
return result;
}
@Override
public JsonNode visitRoot_path(Root_pathContext ctx) {
final String METHOD = "visitRoot_path";
if (LOG.isLoggable(Level.FINEST))
LOG.entering(CLASS, METHOD, ctx.getText());
// [{"a":{"b":1}}, {"a":{"b":2}}].a
final ExprContext lhsCtx = ctx.expr(); // e.g. $$.a.b.c
// reset the stack
int stackSize = _environment.sizeContext();
Stack tmpStack = new Stack();
for (; stackSize > 1; stackSize--) {
tmpStack.push(_environment.popContext());
}
JsonNode result = null;
if (lhsCtx == null) {
result = _environment.peekContext();
} else {
result = visit(lhsCtx);
}
while (!tmpStack.isEmpty()) {
_environment.pushContext(tmpStack.pop());
}
if (LOG.isLoggable(Level.FINEST))
LOG.exiting(CLASS, METHOD, result);
return result;
}
@Override
public JsonNode visitSeq(SeqContext ctx) {
JsonNode start = visit(ctx.expr(0));
JsonNode end = visit(ctx.expr(1));
// if (start == null) {
// throw new EvaluateRuntimeException(ERR_SEQ_LHS_INTEGER);
// }
if (start != null && !isWholeNumber(start)) {
throw new EvaluateRuntimeException(ERR_SEQ_LHS_INTEGER);
}
// if (end == null) {
// throw new EvaluateRuntimeException(ERR_SEQ_RHS_INTEGER);
// }
if (end != null && !isWholeNumber(end)) {
throw new EvaluateRuntimeException(ERR_SEQ_RHS_INTEGER);
}
ArrayNode result = factory.arrayNode();
if (start != null && end != null) {
int iStart = start.asInt();
int iEnd = end.asInt();
int count = iEnd - iStart + 1;
if (iEnd > iStart && count > 10000000) {
// note: below should read 1e7 not 1e6...
throw new EvaluateRuntimeException(ERR_TOO_BIG + count + ".");
}
for (int i = start.asInt(); i <= end.asInt(); i++) {
result.add(new LongNode(i)); // use longs to align with the output of
// visitNumber
}
}
return result;
}
@Override
public JsonNode visitString(MappingExpressionParser.StringContext ctx) {
String val = ctx.getText();
// strip quotes and unescape any special chars
val = sanitise(val);
return TextNode.valueOf(val);
}
@Override
public JsonNode visitTo_array(MappingExpressionParser.To_arrayContext ctx) {
JsonNode result = null;
ExprContext expr = ctx.expr();
if (expr instanceof MappingExpressionParser.PathContext || expr instanceof Context_refContext) {
JsonNode tmpResult = visit(expr);
if (tmpResult instanceof ExpressionsVisitor.SelectorArrayNode) {
result = tmpResult;
} else {
result = JsonNodeFactory.instance.arrayNode();
((ArrayNode) result).add(visit(expr));
}
} else {
result = visit(expr);
}
return result;
}
public JsonNode visitTree(ParseTree tree) {
int treeSize = tree.getChildCount();
steps.clear();
for (int i = 0; i < treeSize; i++) {
steps.add(tree.getChild(i));
}
if (tree.getChild(0) instanceof Array_constructorContext) {
firstStepCons = true;
}
// test for laststep array construction child[n-1] instanceof
// Array_constructorContext
if (tree.getChild(treeSize - 1) instanceof Array_constructorContext) {
lastStepCons = true;
}
if (tree.equals(steps.get(0))) {
firstStep = true;
} else {
firstStep = false;
}
JsonNode result = visit(tree);
// simulates lastStep processing
if (result != null && result.isArray() && result.size() == 1 && ((ArrayNode) result).get(0).isArray()) {
result = ((ArrayNode) result).get(0);
}
if (result != null && result.isDouble()) {
Double d = result.asDouble();
if (Double.isInfinite(d) || Double.isNaN(d)) {
result = JsonNodeFactory.instance.nullNode();
}
}
return result;
}
@Override
public JsonNode visitUnary_op(MappingExpressionParser.Unary_opContext ctx) {
JsonNode result = null;
JsonNode operand = visit(ctx.expr());
if (ctx.op.getType() == MappingExpressionParser.SUB) {
if (operand == null) {
return null; // NOTE: Javascript JSONata engine actually throws an
// exception here (but it shouldn't.. this is a bug in
// that engine that will be fixed)
} else if (operand.isFloatingPointNumber()) {
result = new DoubleNode(-operand.asDouble());
} else if (operand.isIntegralNumber()) {
result = new LongNode(-operand.asLong());
} else {
throw new EvaluateRuntimeException(ERR_NEGATE_NON_NUMERIC);
}
} else {
// cannot happen (expression will not have parsed)
result = null;
}
return result;
}
@Override
public JsonNode visitVar_assign(MappingExpressionParser.Var_assignContext ctx) {
final String varName = ctx.VAR_ID().getText();
JsonNode result = null;
ExprContext expr = ctx.expr();
if (expr instanceof MappingExpressionParser.Function_declContext) {
// build the declared function to assign to this variable in the
// functionMap
MappingExpressionParser.Function_declContext fctDeclCtx = (MappingExpressionParser.Function_declContext) expr;
MappingExpressionParser.VarListContext varList = fctDeclCtx.varList();
MappingExpressionParser.ExprListContext exprList = fctDeclCtx.exprList();
DeclaredFunction fct = new DeclaredFunction(varList, exprList);
setDeclaredFunction(varName, fct);
} else if (expr instanceof MappingExpressionParser.Function_callContext) {
Function_callContext fctCallCtx = (Function_callContext) expr;
String functionName = fctCallCtx.VAR_ID().getText();
DeclaredFunction declFct = getDeclaredFunction(functionName);
if (declFct == null) {
Function function = getJsonataFunction(functionName);
if (function != null) {
setJsonataFunction(varName, function);
// result = function.invoke(this, ctx);
} else {
throw new EvaluateRuntimeException("Unknown function: " + functionName);
}
} else {
setDeclaredFunction(varName, declFct);
// result = declFct.invoke(this, ctx);
}
result = visit(expr);
} else if (expr instanceof Fct_chainContext) {
/**
*
*
* HERE HERE HERE -- consider adding a map of Fct_chainContext keyed by varname
*
*
*/
// need to call the first
ExprContext exprCtx = ((Fct_chainContext) expr).expr(0);
if (exprCtx instanceof Var_recallContext) {
final String fctName = exprCtx.getText();
DeclaredFunction declFct = getDeclaredFunction(fctName);
if (declFct != null) {
setDeclaredFunction(varName, declFct);
// TODO set up an ExprValuesContext with the variable value(s) then call below
// _environment.pushContext(declFct.invoke(this, ruleValues));
result = visit(((Fct_chainContext) expr).expr(1));
// _environment.popContext();
} else {
Function fct = getJsonataFunction(fctName);
if (fct != null) {
setJsonataFunction(varName, fct);
// TODO set up a Function_callContext with an ExprValuesContext and call below
// with it instead of ctx
// _environment.pushContext(fct.invoke(this, ctx));
result = visit(((Fct_chainContext) expr).expr(1));
// _environment.popContext();
} else {
result = null;
}
}
}
} else {
result = visit(expr);
setVariable(varName, result);
}
return result;
}
@Override
public JsonNode visitVar_recall(MappingExpressionParser.Var_recallContext ctx) {
final String varName = ctx.getText();
JsonNode result = getVariable(varName);
return result;
}
}