Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@
<version>7.0.8</version>
<scope>test</scope>
</dependency>
<!-- Pure-Java RE2 port, used only to demonstrate plugging in a linear-time
(ReDoS-safe) RegexEngine; see RE2RegexPattern / RE2RegexPatternTest. -->
<dependency>
<groupId>com.google.re2j</groupId>
<artifactId>re2j</artifactId>
<version>1.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
Expand Down
47 changes: 44 additions & 3 deletions src/main/java/com/api/jsonata4java/Expression.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.api.jsonata4java.expressions.ParseException;
import com.api.jsonata4java.expressions.functions.DeclaredFunction;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.ExprContext;
import com.api.jsonata4java.expressions.regex.RegexEngine;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
Expand Down Expand Up @@ -90,6 +91,25 @@ public static Expression jsonata(String expression) throws ParseException, IOExc
return new Expression(expression);
}

/**
* Generate a new Expression based on evaluating the supplied expression,
* using a custom regex engine to compile any regex literals or dynamic
* string patterns encountered (e.g. for $match/$replace/$split) instead of
* the default {@link java.util.regex.Pattern}-backed one.
*
* @param expression
* the logic to be parsed for later execution via the evaluate
* methods
* @param regexEngine
* the regex engine to use
* @return new Expression object
* @throws ParseException
* @throws IOException
*/
public static Expression jsonata(String expression, RegexEngine regexEngine) throws ParseException, IOException {
return new Expression(expression, regexEngine);
}

/**
* Testing the various methods based on
* https://docs.jsonata.org/embedding-extending#expressionregisterfunctionname-implementation-signature
Expand Down Expand Up @@ -143,10 +163,11 @@ public static void main(String[] args) {
ExpressionsVisitor _eval = null;
Expressions _expr = null;
Map<String, ExprContext> _variableMap = new HashMap<String, ExprContext>();
RegexEngine _regexEngine = RegexEngine.defaultEngine();

/**
* Constructor for Expression
*
*
* @param expression
* the logic to be parsed for later execution via evaluate
* methods
Expand All @@ -158,6 +179,26 @@ public Expression(String expression) throws ParseException, IOException {
_eval = _expr.getExpr();
}

/**
* Constructor for Expression, using a custom regex engine to compile any
* regex literals or dynamic string patterns encountered (e.g. for
* $match/$replace/$split) instead of the default
* {@link java.util.regex.Pattern}-backed one.
*
* @param expression
* the logic to be parsed for later execution via evaluate
* methods
* @param regexEngine
* the regex engine to use
* @throws ParseException
* @throws IOException
*/
public Expression(String expression, RegexEngine regexEngine) throws ParseException, IOException {
_regexEngine = regexEngine != null ? regexEngine : RegexEngine.defaultEngine();
_expr = Expressions.parse(expression, _regexEngine);
_eval = _expr.getExpr();
}

/**
* Assign the binding to the environment preparing for evaluation
*
Expand Down Expand Up @@ -210,7 +251,7 @@ public void clear() {
* @throws ParseException
*/
public JsonNode evaluate(JsonNode rootContext) throws ParseException {
ExpressionsVisitor eval = new ExpressionsVisitor(rootContext, new FrameEnvironment(null));
ExpressionsVisitor eval = new ExpressionsVisitor(rootContext, new FrameEnvironment(null), _regexEngine);
// process any stored bindings
for (Iterator<String> it = _variableMap.keySet().iterator(); it.hasNext();) {
String key = it.next();
Expand Down Expand Up @@ -359,7 +400,7 @@ public JsonNode evaluate(JsonNode rootContext, List<Binding> bindings, long time
* If the given device event is invalid.
*/
public JsonNode evaluate(JsonNode rootContext, long timeoutMS, int maxDepth) throws EvaluateException {
ExpressionsVisitor eval = new ExpressionsVisitor(rootContext, new FrameEnvironment(null));
ExpressionsVisitor eval = new ExpressionsVisitor(rootContext, new FrameEnvironment(null), _regexEngine);
// process any stored bindings
for (Iterator<String> it = _variableMap.keySet().iterator(); it.hasNext();) {
String key = it.next();
Expand Down
28 changes: 26 additions & 2 deletions src/main/java/com/api/jsonata4java/expressions/Expressions.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.api.jsonata4java.expressions.generated.MappingExpressionParser;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.ExprContext;
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Expr_to_eofContext;
import com.api.jsonata4java.expressions.regex.RegexEngine;
import com.api.jsonata4java.expressions.utils.Constants;
import com.fasterxml.jackson.databind.JsonNode;

Expand Down Expand Up @@ -75,6 +76,25 @@ public static List<String> getRefsInExpression(Pattern refPattern, String expres
// Convert a mapping expression string into a pre-processed expression ready
// for evaluation
public static Expressions parse(String mappingExpression) throws ParseException, IOException {
return parse(mappingExpression, RegexEngine.defaultEngine());
}

/**
* Convert a mapping expression string into a pre-processed expression ready
* for evaluation, using a custom regex engine to compile any regex literals
* or dynamic string patterns encountered (e.g. for $match/$replace/$split)
* instead of the default {@link java.util.regex.Pattern}-backed one.
*
* @param mappingExpression
* the expression to parse
* @param regexEngine
* the regex engine to use
* @return the parsed expression
* @throws ParseException
* @throws IOException
*/
public static Expressions parse(String mappingExpression, RegexEngine regexEngine)
throws ParseException, IOException {

// Expressions can include references to properties within an
// application interface ("state"),
Expand Down Expand Up @@ -119,7 +139,7 @@ public static Expressions parse(String mappingExpression) throws ParseException,
throw new ParseException(e.getMessage());
}

return new Expressions(newTree, mappingExpression);
return new Expressions(newTree, mappingExpression, regexEngine);
}

ExpressionsVisitor _eval = null;
Expand All @@ -129,7 +149,11 @@ public static Expressions parse(String mappingExpression) throws ParseException,
ParseTree _tree = null;

public Expressions(ParseTree aTree, String anExpression) {
_eval = new ExpressionsVisitor(null, new FrameEnvironment(null));
this(aTree, anExpression, RegexEngine.defaultEngine());
}

public Expressions(ParseTree aTree, String anExpression, RegexEngine regexEngine) {
_eval = new ExpressionsVisitor(null, new FrameEnvironment(null), regexEngine);
_tree = aTree;
_expression = anExpression;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
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.regex.RegexEngine;
import com.api.jsonata4java.expressions.utils.BooleanUtils;
import com.api.jsonata4java.expressions.utils.Constants;
import com.api.jsonata4java.expressions.utils.FunctionUtils;
Expand Down Expand Up @@ -437,6 +438,7 @@ public static JsonNode unwrapArray(JsonNode input) {
private List<ParseTree> steps = new ArrayList<ParseTree>();
private ParseTreeProperty<Integer> values = new ParseTreeProperty<Integer>();
private String lastVisited = "";
private RegexEngine regexEngine = RegexEngine.defaultEngine();

public ExpressionsVisitor() {
setEnvironment(null);
Expand All @@ -448,6 +450,29 @@ public ExpressionsVisitor(JsonNode rootContext, FrameEnvironment environment) th
setRootContext(rootContext);
}

public ExpressionsVisitor(JsonNode rootContext, FrameEnvironment environment, RegexEngine regexEngine)
throws EvaluateRuntimeException {
setEnvironment(environment);
setRootContext(rootContext);
if (regexEngine != null) {
this.regexEngine = regexEngine;
}
}

/**
* @return the regex engine used to compile JSONata regex literals and
* dynamic string patterns (e.g. for $match/$replace/$split).
*/
public RegexEngine getRegexEngine() {
return regexEngine;
}

public void setRegexEngine(RegexEngine regexEngine) {
if (regexEngine != null) {
this.regexEngine = regexEngine;
}
}

public FrameEnvironment setNewEnvironment() {
// generate a new frameEnvironment for life of this block
FrameEnvironment oldEnvironment = _environment;
Expand Down Expand Up @@ -2732,7 +2757,7 @@ public JsonNode visitRegular_expression(MappingExpressionParser.Regular_expressi
}
JsonNode result = null;
if (ctx.getText() != null) {
final RegularExpression regex = new RegularExpression(ctx.getText());
final RegularExpression regex = new RegularExpression(ctx.getText(), regexEngine);
result = new POJONode(regex);
lastVisited = METHOD;
}
Expand All @@ -2754,7 +2779,7 @@ public JsonNode visitRegular_expression_caseinsensitive(
JsonNode result = null;
if (ctx.getText() != null) {
final RegularExpression regex = new RegularExpression(RegularExpression.Type.CASEINSENSITIVE,
ctx.getText());
ctx.getText(), regexEngine);
result = new POJONode(regex);
lastVisited = METHOD;
}
Expand All @@ -2774,7 +2799,8 @@ public JsonNode visitRegular_expression_multiline(MappingExpressionParser.Regula
}
JsonNode result = null;
if (ctx.getText() != null) {
final RegularExpression regex = new RegularExpression(RegularExpression.Type.MULTILINE, ctx.getText());
final RegularExpression regex = new RegularExpression(RegularExpression.Type.MULTILINE, ctx.getText(),
regexEngine);
result = new POJONode(regex);
lastVisited = METHOD;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@

package com.api.jsonata4java.expressions;

import java.util.regex.Pattern;
import com.api.jsonata4java.expressions.regex.RegexEngine;
import com.api.jsonata4java.expressions.regex.RegexFlags;
import com.api.jsonata4java.expressions.regex.RegexPattern;

/**
* A helper class to store information about a parsed regular expression.
*
*
* @author Martin Bluemel
*/
public class RegularExpression {
Expand All @@ -39,13 +41,21 @@ public enum Type {

private String regexPattern;

private Pattern pattern;
private RegexPattern pattern;

public RegularExpression(String string) {
this(Type.NORMAL, string);
this(Type.NORMAL, string, RegexEngine.defaultEngine());
}

public RegularExpression(final Type type, final String regex) {
this(type, regex, RegexEngine.defaultEngine());
}

public RegularExpression(String string, RegexEngine engine) {
this(Type.NORMAL, string, engine);
}

public RegularExpression(final Type type, final String regex, final RegexEngine engine) {
this.type = type;
switch (type) {
case CASEINSENSITIVE:
Expand All @@ -56,21 +66,23 @@ public RegularExpression(final Type type, final String regex) {
regexPattern = regex.substring(1, regex.length() - 1);
break;
}
compile();
compile(engine);
}

private void compile() {
private void compile(final RegexEngine engine) {
final RegexFlags flags;
switch (this.type) {
case CASEINSENSITIVE:
this.pattern = Pattern.compile(regexPattern, Pattern.CASE_INSENSITIVE);
flags = new RegexFlags(true, false);
break;
case MULTILINE:
this.pattern = Pattern.compile(regexPattern, Pattern.MULTILINE);
flags = new RegexFlags(false, true);
break;
default:
this.pattern = Pattern.compile(regexPattern);
flags = new RegexFlags(false, false);
break;
}
this.pattern = engine.compile(regexPattern, flags);
}

@Override
Expand All @@ -82,7 +94,11 @@ public Type getType() {
return this.type;
}

public Pattern getPattern() {
/**
* @return the compiled regex, using whichever {@link RegexEngine} this
* instance was constructed with.
*/
public RegexPattern getPattern() {
return this.pattern;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public JsonNode invoke(ExpressionsVisitor expressionVisitor, Function_callContex
} else if (argPattern instanceof POJONode) {
// Match against a regular expression
final RegularExpression regex = ((RegularExpression) ((POJONode) argPattern).getPojo());
result = regex.getPattern().matcher(str).find() ? BooleanNode.TRUE : BooleanNode.FALSE;
result = regex.getPattern().test(str) ? BooleanNode.TRUE : BooleanNode.FALSE;
} else {
throw new EvaluateRuntimeException(ERR_ARG2BADTYPE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ public JsonNode invoke(ExpressionsVisitor expressionVisitor, Function_callContex
context = FunctionUtils.getValuesListExpression(expressionVisitor, ctx, useContext ? 0 : 1);
}
try {
Expression expr = Expression.jsonata(expression);
// Use the enclosing expression's regex engine so regex literals
// inside the eval'd expression are compiled consistently with
// the rest of the enclosing expression (e.g. RE2 instead of the
// default java.util.regex).
Expression expr = Expression.jsonata(expression, expressionVisitor.getRegexEngine());
result = expr.evaluate(context);
} catch (ParseException | IOException e) {
throw new EvaluateRuntimeException(ERR_ARG1BADTYPE);
Expand Down
Loading