Skip to content

Commit 3416fe7

Browse files
authored
Add pluggable regex engine support (#428)
Signed-off-by: Robert Yokota <rayokota@gmail.com> Thank you for your work.
1 parent 9940185 commit 3416fe7

18 files changed

Lines changed: 864 additions & 89 deletions

pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@
101101
<version>7.0.8</version>
102102
<scope>test</scope>
103103
</dependency>
104+
<!-- Pure-Java RE2 port, used only to demonstrate plugging in a linear-time
105+
(ReDoS-safe) RegexEngine; see RE2RegexPattern / RE2RegexPatternTest. -->
106+
<dependency>
107+
<groupId>com.google.re2j</groupId>
108+
<artifactId>re2j</artifactId>
109+
<version>1.8</version>
110+
<scope>test</scope>
111+
</dependency>
104112
<dependency>
105113
<groupId>org.antlr</groupId>
106114
<artifactId>antlr4-runtime</artifactId>

src/main/java/com/api/jsonata4java/Expression.java

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.api.jsonata4java.expressions.ParseException;
2020
import com.api.jsonata4java.expressions.functions.DeclaredFunction;
2121
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.ExprContext;
22+
import com.api.jsonata4java.expressions.regex.RegexEngine;
2223
import com.fasterxml.jackson.databind.JsonNode;
2324
import com.fasterxml.jackson.databind.ObjectMapper;
2425
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
@@ -90,6 +91,25 @@ public static Expression jsonata(String expression) throws ParseException, IOExc
9091
return new Expression(expression);
9192
}
9293

94+
/**
95+
* Generate a new Expression based on evaluating the supplied expression,
96+
* using a custom regex engine to compile any regex literals or dynamic
97+
* string patterns encountered (e.g. for $match/$replace/$split) instead of
98+
* the default {@link java.util.regex.Pattern}-backed one.
99+
*
100+
* @param expression
101+
* the logic to be parsed for later execution via the evaluate
102+
* methods
103+
* @param regexEngine
104+
* the regex engine to use
105+
* @return new Expression object
106+
* @throws ParseException
107+
* @throws IOException
108+
*/
109+
public static Expression jsonata(String expression, RegexEngine regexEngine) throws ParseException, IOException {
110+
return new Expression(expression, regexEngine);
111+
}
112+
93113
/**
94114
* Testing the various methods based on
95115
* https://docs.jsonata.org/embedding-extending#expressionregisterfunctionname-implementation-signature
@@ -143,10 +163,11 @@ public static void main(String[] args) {
143163
ExpressionsVisitor _eval = null;
144164
Expressions _expr = null;
145165
Map<String, ExprContext> _variableMap = new HashMap<String, ExprContext>();
166+
RegexEngine _regexEngine = RegexEngine.defaultEngine();
146167

147168
/**
148169
* Constructor for Expression
149-
*
170+
*
150171
* @param expression
151172
* the logic to be parsed for later execution via evaluate
152173
* methods
@@ -158,6 +179,26 @@ public Expression(String expression) throws ParseException, IOException {
158179
_eval = _expr.getExpr();
159180
}
160181

182+
/**
183+
* Constructor for Expression, using a custom regex engine to compile any
184+
* regex literals or dynamic string patterns encountered (e.g. for
185+
* $match/$replace/$split) instead of the default
186+
* {@link java.util.regex.Pattern}-backed one.
187+
*
188+
* @param expression
189+
* the logic to be parsed for later execution via evaluate
190+
* methods
191+
* @param regexEngine
192+
* the regex engine to use
193+
* @throws ParseException
194+
* @throws IOException
195+
*/
196+
public Expression(String expression, RegexEngine regexEngine) throws ParseException, IOException {
197+
_regexEngine = regexEngine != null ? regexEngine : RegexEngine.defaultEngine();
198+
_expr = Expressions.parse(expression, _regexEngine);
199+
_eval = _expr.getExpr();
200+
}
201+
161202
/**
162203
* Assign the binding to the environment preparing for evaluation
163204
*
@@ -210,7 +251,7 @@ public void clear() {
210251
* @throws ParseException
211252
*/
212253
public JsonNode evaluate(JsonNode rootContext) throws ParseException {
213-
ExpressionsVisitor eval = new ExpressionsVisitor(rootContext, new FrameEnvironment(null));
254+
ExpressionsVisitor eval = new ExpressionsVisitor(rootContext, new FrameEnvironment(null), _regexEngine);
214255
// process any stored bindings
215256
for (Iterator<String> it = _variableMap.keySet().iterator(); it.hasNext();) {
216257
String key = it.next();
@@ -359,7 +400,7 @@ public JsonNode evaluate(JsonNode rootContext, List<Binding> bindings, long time
359400
* If the given device event is invalid.
360401
*/
361402
public JsonNode evaluate(JsonNode rootContext, long timeoutMS, int maxDepth) throws EvaluateException {
362-
ExpressionsVisitor eval = new ExpressionsVisitor(rootContext, new FrameEnvironment(null));
403+
ExpressionsVisitor eval = new ExpressionsVisitor(rootContext, new FrameEnvironment(null), _regexEngine);
363404
// process any stored bindings
364405
for (Iterator<String> it = _variableMap.keySet().iterator(); it.hasNext();) {
365406
String key = it.next();

src/main/java/com/api/jsonata4java/expressions/Expressions.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import com.api.jsonata4java.expressions.generated.MappingExpressionParser;
4242
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.ExprContext;
4343
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Expr_to_eofContext;
44+
import com.api.jsonata4java.expressions.regex.RegexEngine;
4445
import com.api.jsonata4java.expressions.utils.Constants;
4546
import com.fasterxml.jackson.databind.JsonNode;
4647

@@ -75,6 +76,25 @@ public static List<String> getRefsInExpression(Pattern refPattern, String expres
7576
// Convert a mapping expression string into a pre-processed expression ready
7677
// for evaluation
7778
public static Expressions parse(String mappingExpression) throws ParseException, IOException {
79+
return parse(mappingExpression, RegexEngine.defaultEngine());
80+
}
81+
82+
/**
83+
* Convert a mapping expression string into a pre-processed expression ready
84+
* for evaluation, using a custom regex engine to compile any regex literals
85+
* or dynamic string patterns encountered (e.g. for $match/$replace/$split)
86+
* instead of the default {@link java.util.regex.Pattern}-backed one.
87+
*
88+
* @param mappingExpression
89+
* the expression to parse
90+
* @param regexEngine
91+
* the regex engine to use
92+
* @return the parsed expression
93+
* @throws ParseException
94+
* @throws IOException
95+
*/
96+
public static Expressions parse(String mappingExpression, RegexEngine regexEngine)
97+
throws ParseException, IOException {
7898

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

122-
return new Expressions(newTree, mappingExpression);
142+
return new Expressions(newTree, mappingExpression, regexEngine);
123143
}
124144

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

131151
public Expressions(ParseTree aTree, String anExpression) {
132-
_eval = new ExpressionsVisitor(null, new FrameEnvironment(null));
152+
this(aTree, anExpression, RegexEngine.defaultEngine());
153+
}
154+
155+
public Expressions(ParseTree aTree, String anExpression, RegexEngine regexEngine) {
156+
_eval = new ExpressionsVisitor(null, new FrameEnvironment(null), regexEngine);
133157
_tree = aTree;
134158
_expression = anExpression;
135159
}

src/main/java/com/api/jsonata4java/expressions/ExpressionsVisitor.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.SeqContext;
7171
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.StringContext;
7272
import com.api.jsonata4java.expressions.generated.MappingExpressionParser.Var_recallContext;
73+
import com.api.jsonata4java.expressions.regex.RegexEngine;
7374
import com.api.jsonata4java.expressions.utils.BooleanUtils;
7475
import com.api.jsonata4java.expressions.utils.Constants;
7576
import com.api.jsonata4java.expressions.utils.FunctionUtils;
@@ -437,6 +438,7 @@ public static JsonNode unwrapArray(JsonNode input) {
437438
private List<ParseTree> steps = new ArrayList<ParseTree>();
438439
private ParseTreeProperty<Integer> values = new ParseTreeProperty<Integer>();
439440
private String lastVisited = "";
441+
private RegexEngine regexEngine = RegexEngine.defaultEngine();
440442

441443
public ExpressionsVisitor() {
442444
setEnvironment(null);
@@ -448,6 +450,29 @@ public ExpressionsVisitor(JsonNode rootContext, FrameEnvironment environment) th
448450
setRootContext(rootContext);
449451
}
450452

453+
public ExpressionsVisitor(JsonNode rootContext, FrameEnvironment environment, RegexEngine regexEngine)
454+
throws EvaluateRuntimeException {
455+
setEnvironment(environment);
456+
setRootContext(rootContext);
457+
if (regexEngine != null) {
458+
this.regexEngine = regexEngine;
459+
}
460+
}
461+
462+
/**
463+
* @return the regex engine used to compile JSONata regex literals and
464+
* dynamic string patterns (e.g. for $match/$replace/$split).
465+
*/
466+
public RegexEngine getRegexEngine() {
467+
return regexEngine;
468+
}
469+
470+
public void setRegexEngine(RegexEngine regexEngine) {
471+
if (regexEngine != null) {
472+
this.regexEngine = regexEngine;
473+
}
474+
}
475+
451476
public FrameEnvironment setNewEnvironment() {
452477
// generate a new frameEnvironment for life of this block
453478
FrameEnvironment oldEnvironment = _environment;
@@ -2732,7 +2757,7 @@ public JsonNode visitRegular_expression(MappingExpressionParser.Regular_expressi
27322757
}
27332758
JsonNode result = null;
27342759
if (ctx.getText() != null) {
2735-
final RegularExpression regex = new RegularExpression(ctx.getText());
2760+
final RegularExpression regex = new RegularExpression(ctx.getText(), regexEngine);
27362761
result = new POJONode(regex);
27372762
lastVisited = METHOD;
27382763
}
@@ -2754,7 +2779,7 @@ public JsonNode visitRegular_expression_caseinsensitive(
27542779
JsonNode result = null;
27552780
if (ctx.getText() != null) {
27562781
final RegularExpression regex = new RegularExpression(RegularExpression.Type.CASEINSENSITIVE,
2757-
ctx.getText());
2782+
ctx.getText(), regexEngine);
27582783
result = new POJONode(regex);
27592784
lastVisited = METHOD;
27602785
}
@@ -2774,7 +2799,8 @@ public JsonNode visitRegular_expression_multiline(MappingExpressionParser.Regula
27742799
}
27752800
JsonNode result = null;
27762801
if (ctx.getText() != null) {
2777-
final RegularExpression regex = new RegularExpression(RegularExpression.Type.MULTILINE, ctx.getText());
2802+
final RegularExpression regex = new RegularExpression(RegularExpression.Type.MULTILINE, ctx.getText(),
2803+
regexEngine);
27782804
result = new POJONode(regex);
27792805
lastVisited = METHOD;
27802806
}

src/main/java/com/api/jsonata4java/expressions/RegularExpression.java

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222

2323
package com.api.jsonata4java.expressions;
2424

25-
import java.util.regex.Pattern;
25+
import com.api.jsonata4java.expressions.regex.RegexEngine;
26+
import com.api.jsonata4java.expressions.regex.RegexFlags;
27+
import com.api.jsonata4java.expressions.regex.RegexPattern;
2628

2729
/**
2830
* A helper class to store information about a parsed regular expression.
29-
*
31+
*
3032
* @author Martin Bluemel
3133
*/
3234
public class RegularExpression {
@@ -39,13 +41,21 @@ public enum Type {
3941

4042
private String regexPattern;
4143

42-
private Pattern pattern;
44+
private RegexPattern pattern;
4345

4446
public RegularExpression(String string) {
45-
this(Type.NORMAL, string);
47+
this(Type.NORMAL, string, RegexEngine.defaultEngine());
4648
}
4749

4850
public RegularExpression(final Type type, final String regex) {
51+
this(type, regex, RegexEngine.defaultEngine());
52+
}
53+
54+
public RegularExpression(String string, RegexEngine engine) {
55+
this(Type.NORMAL, string, engine);
56+
}
57+
58+
public RegularExpression(final Type type, final String regex, final RegexEngine engine) {
4959
this.type = type;
5060
switch (type) {
5161
case CASEINSENSITIVE:
@@ -56,21 +66,23 @@ public RegularExpression(final Type type, final String regex) {
5666
regexPattern = regex.substring(1, regex.length() - 1);
5767
break;
5868
}
59-
compile();
69+
compile(engine);
6070
}
6171

62-
private void compile() {
72+
private void compile(final RegexEngine engine) {
73+
final RegexFlags flags;
6374
switch (this.type) {
6475
case CASEINSENSITIVE:
65-
this.pattern = Pattern.compile(regexPattern, Pattern.CASE_INSENSITIVE);
76+
flags = new RegexFlags(true, false);
6677
break;
6778
case MULTILINE:
68-
this.pattern = Pattern.compile(regexPattern, Pattern.MULTILINE);
79+
flags = new RegexFlags(false, true);
6980
break;
7081
default:
71-
this.pattern = Pattern.compile(regexPattern);
82+
flags = new RegexFlags(false, false);
7283
break;
7384
}
85+
this.pattern = engine.compile(regexPattern, flags);
7486
}
7587

7688
@Override
@@ -82,7 +94,11 @@ public Type getType() {
8294
return this.type;
8395
}
8496

85-
public Pattern getPattern() {
97+
/**
98+
* @return the compiled regex, using whichever {@link RegexEngine} this
99+
* instance was constructed with.
100+
*/
101+
public RegexPattern getPattern() {
86102
return this.pattern;
87103
}
88104
}

src/main/java/com/api/jsonata4java/expressions/functions/ContainsFunction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public JsonNode invoke(ExpressionsVisitor expressionVisitor, Function_callContex
118118
} else if (argPattern instanceof POJONode) {
119119
// Match against a regular expression
120120
final RegularExpression regex = ((RegularExpression) ((POJONode) argPattern).getPojo());
121-
result = regex.getPattern().matcher(str).find() ? BooleanNode.TRUE : BooleanNode.FALSE;
121+
result = regex.getPattern().test(str) ? BooleanNode.TRUE : BooleanNode.FALSE;
122122
} else {
123123
throw new EvaluateRuntimeException(ERR_ARG2BADTYPE);
124124
}

src/main/java/com/api/jsonata4java/expressions/functions/EvalFunction.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ public JsonNode invoke(ExpressionsVisitor expressionVisitor, Function_callContex
7777
context = FunctionUtils.getValuesListExpression(expressionVisitor, ctx, useContext ? 0 : 1);
7878
}
7979
try {
80-
Expression expr = Expression.jsonata(expression);
80+
// Use the enclosing expression's regex engine so regex literals
81+
// inside the eval'd expression are compiled consistently with
82+
// the rest of the enclosing expression (e.g. RE2 instead of the
83+
// default java.util.regex).
84+
Expression expr = Expression.jsonata(expression, expressionVisitor.getRegexEngine());
8185
result = expr.evaluate(context);
8286
} catch (ParseException | IOException e) {
8387
throw new EvaluateRuntimeException(ERR_ARG1BADTYPE);

0 commit comments

Comments
 (0)