/*
* Copyright 2022 the original author or authors.
*
* 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
*
* https://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 org.openrewrite.hcl;
import lombok.EqualsAndHashCode;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.openrewrite.Cursor;
import org.openrewrite.Tree;
import org.openrewrite.hcl.internal.grammar.JsonPathLexer;
import org.openrewrite.hcl.internal.grammar.JsonPathParser;
import org.openrewrite.hcl.internal.grammar.JsonPathParserBaseVisitor;
import org.openrewrite.hcl.internal.grammar.JsonPathParserVisitor;
import org.openrewrite.hcl.tree.Hcl;
import org.openrewrite.internal.lang.Nullable;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.util.Collections.disjoint;
/**
* Provides methods for matching the given cursor location to a specific JsonPath expression.
*
* This is not a full implementation of the JsonPath syntax as linked in the "see also."
*
* @see https://support.smartbear.com/alertsite/docs/monitors/api/endpoint/jsonpath.html
*/
@EqualsAndHashCode
public class JsonPathMatcher {
private final String jsonPath;
public JsonPathMatcher(String jsonPath) {
this.jsonPath = jsonPath;
}
public Optional find(Cursor cursor) {
LinkedList cursorPath = cursor.getPathAsStream()
.filter(o -> o instanceof Tree)
.map(Tree.class::cast)
.collect(Collectors.toCollection(LinkedList::new));
if (cursorPath.isEmpty()) {
return Optional.empty();
}
Collections.reverse(cursorPath);
Tree start;
if (jsonPath.startsWith(".") && !jsonPath.startsWith("..")) {
start = cursor.getValue();
} else {
start = cursorPath.peekFirst();
}
JsonPathParser.JsonPathContext ctx = jsonPath().jsonPath();
// The stop may be optimized by interpreting the ExpressionContext and pre-determining the last visit.
JsonPathParser.ExpressionContext stop = (JsonPathParser.ExpressionContext) ctx.children.get(ctx.children.size() - 1);
@SuppressWarnings("ConstantConditions") JsonPathParserVisitor v = new JsonPathParserHclVisitor(cursorPath, start, stop, false);
Object result = v.visit(ctx);
//noinspection unchecked
return Optional.ofNullable((T) result);
}
public boolean matches(Cursor cursor) {
List cursorPath = cursor.getPathAsStream().collect(Collectors.toList());
return find(cursor).map(o -> {
if (o instanceof List) {
//noinspection unchecked
List l = (List) o;
return !disjoint(l, cursorPath) && l.contains(cursor.getValue());
} else {
return Objects.equals(o, cursor.getValue());
}
}).orElse(false);
}
private JsonPathParser jsonPath() {
return new JsonPathParser(new CommonTokenStream(new JsonPathLexer(CharStreams.fromString(this.jsonPath))));
}
@SuppressWarnings({"ConstantConditions", "unchecked"})
private static class JsonPathParserHclVisitor extends JsonPathParserBaseVisitor {
private final List cursorPath;
protected Object scope;
private final JsonPathParser.ExpressionContext stop;
private final boolean isRecursiveDescent;
public JsonPathParserHclVisitor(List cursorPath, Object scope, JsonPathParser.ExpressionContext stop, boolean isRecursiveDescent) {
this.cursorPath = cursorPath;
this.scope = scope;
this.stop = stop;
this.isRecursiveDescent = isRecursiveDescent;
}
@Override
protected Object defaultResult() {
return scope;
}
@Override
protected Object aggregateResult(Object aggregate, Object nextResult) {
return (scope = nextResult);
}
@Override
public Object visitJsonPath(JsonPathParser.JsonPathContext ctx) {
if (ctx.ROOT() != null || "[".equals(ctx.start.getText())) {
scope = cursorPath.stream()
.filter(t -> t instanceof Hcl.Block)
.findFirst()
.orElseGet(() -> cursorPath.stream()
.filter(t -> t instanceof Hcl.ConfigFile)
.findFirst()
.orElse(null));
}
return super.visitJsonPath(ctx);
}
@Override
public Object visitRecursiveDecent(JsonPathParser.RecursiveDecentContext ctx) {
if (scope == null) {
return null;
}
Object result = null;
// A recursive descent at the start of the expression or declared in a filter must check the entire cursor patch.
// `$..foo` or `$.foo..bar[?($..buz == 'buz')]`
List previous = ctx.getParent().getParent().children;
ParserRuleContext current = ctx.getParent();
if (previous.indexOf(current) - 1 < 0 || "$".equals(previous.get(previous.indexOf(current) - 1).getText())) {
List results = new ArrayList<>();
for (Tree path : cursorPath) {
JsonPathParserHclVisitor v = new JsonPathParserHclVisitor(cursorPath, path, null, false);
for (int i = 1; i < ctx.getChildCount(); i++) {
result = v.visit(ctx.getChild(i));
if (result != null) {
results.add(result);
}
}
}
return results;
// Otherwise, the recursive descent is scoped to the previous match. `$.foo..['find-in-foo']`.
} else {
JsonPathParserHclVisitor v = new JsonPathParserHclVisitor(cursorPath, scope, null, true);
for (int i = 1; i < ctx.getChildCount(); i++) {
result = v.visit(ctx.getChild(i));
if (result != null) {
break;
}
}
}
return result;
}
@Override
public Object visitBracketOperator(JsonPathParser.BracketOperatorContext ctx) {
if (!ctx.property().isEmpty()) {
if (ctx.property().size() == 1) {
return visitProperty(ctx.property(0));
}
// Return a list if more than 1 property is specified.
return ctx.property().stream()
.map(this::visitProperty)
.collect(Collectors.toList());
} else if (ctx.slice() != null) {
return visitSlice(ctx.slice());
} else if (ctx.indexes() != null) {
return visitIndexes(ctx.indexes());
} else if (ctx.filter() != null) {
return visitFilter(ctx.filter());
}
return null;
}
@Override
public Object visitSlice(JsonPathParser.SliceContext ctx) {
List results;
if (scope instanceof List) {
//noinspection unchecked
results = (List) scope;
} else if (scope instanceof Hcl.Attribute) {
scope = ((Hcl.Attribute) scope).getValue();
return visitSlice(ctx);
} else {
results = new ArrayList<>();
}
// A wildcard will use these initial values, so it is not checked in the conditions.
int start = 0;
int limit = Integer.MAX_VALUE;
if (ctx.PositiveNumber() != null) {
// [:n], Selects the first n elements of the array.
limit = Integer.parseInt(ctx.PositiveNumber().getText());
} else if (ctx.NegativeNumber() != null) {
// [-n:], Selects the last n elements of the array.
start = results.size() + Integer.parseInt(ctx.NegativeNumber().getText());
} else if (ctx.start() != null) {
// [start:end] or [start:]
// Selects array elements from the start index and up to, but not including, end index.
// If end is omitted, selects all elements from start until the end of the array.
start = ctx.start() != null ? Integer.parseInt(ctx.start().getText()) : 0;
limit = ctx.end() != null ? Integer.parseInt(ctx.end().getText()) + 1 : limit;
}
return results.stream()
.skip(start)
.limit(limit)
.collect(Collectors.toList());
}
@Override
public Object visitIndexes(JsonPathParser.IndexesContext ctx) {
List results;
if (scope instanceof List) {
//noinspection unchecked
results = (List) scope;
} else if (scope instanceof Hcl.Attribute) {
scope = ((Hcl.Attribute) scope).getValue();
return visitIndexes(ctx);
} else {
results = new ArrayList<>();
}
List indexes = new ArrayList<>();
for (TerminalNode terminalNode : ctx.PositiveNumber()) {
for (int i = 0; i < results.size(); i++) {
if (terminalNode.getText().contains(String.valueOf(i))) {
indexes.add(results.get(i));
}
}
}
return getResultFromList(indexes);
}
@Override
public Object visitProperty(JsonPathParser.PropertyContext ctx) {
if (scope instanceof Hcl.Block) {
Hcl.Block block = (Hcl.Block) scope;
List matches = new ArrayList<>();
String key = block.getType().getName();
String name = ctx.StringLiteral() != null ?
unquoteStringLiteral(ctx.StringLiteral().getText()) : ctx.Identifier().getText();
if (isRecursiveDescent) {
if (key.equals(name)) {
matches.add(block);
}
scope = block.getBody();
Object result = getResultFromList(visitProperty(ctx));
if (result != null) {
matches.add(result);
}
return getResultFromList(matches);
} else if (key.equals(name)) {
if (stop != null && getExpressionContext(ctx) == stop) {
return block;
}
scope = block.getBody();
return block.getBody();
}
} else if (scope instanceof Hcl.Attribute) {
Hcl.Attribute attribute = (Hcl.Attribute) scope;
String key = attribute.getSimpleName();
String name = ctx.StringLiteral() != null ?
unquoteStringLiteral(ctx.StringLiteral().getText()) : ctx.Identifier().getText();
if (key.equals(name)) {
return attribute;
}
} else if (scope instanceof List) {
List results = ((List) scope).stream()
.map(o -> {
scope = o;
return visitProperty(ctx);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
// Unwrap lists of results from visitProperty to match the position of the cursor.
List matches = new ArrayList<>();
for (Object result : results) {
if (result instanceof List) {
matches.addAll(((List) result));
} else {
matches.add(result);
}
}
return getResultFromList(matches);
}
return null;
}
private JsonPathParser.ExpressionContext getExpressionContext(ParserRuleContext ctx) {
if (ctx == null || ctx instanceof JsonPathParser.ExpressionContext) {
return (JsonPathParser.ExpressionContext) ctx;
}
return getExpressionContext(ctx.getParent());
}
@Override
public Object visitWildcard(JsonPathParser.WildcardContext ctx) {
if (scope instanceof Hcl.Attribute) {
Hcl.Attribute attr = (Hcl.Attribute) scope;
return attr.getValue();
} else if (scope instanceof List) {
List results = ((List) scope).stream()
.map(o -> {
scope = o;
return visitWildcard(ctx);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
List matches = new ArrayList<>();
if (stop != null && stop == getExpressionContext(ctx)) {
// Return the values of each result when the JsonPath ends with a wildcard.
results.forEach(o -> matches.add(getValue(o)));
} else {
// Unwrap lists of results from visitProperty to match the position of the cursor.
for (Object result : results) {
if (result instanceof List) {
matches.addAll(((List) result));
} else {
matches.add(result);
}
}
}
return getResultFromList(matches);
} else if (scope instanceof Hcl.Block) {
Hcl.Block block = (Hcl.Block) scope;
if (stop != null && getExpressionContext(ctx) == stop) {
return block;
}
scope = block.getBody();
return block.getBody();
}
return null;
}
@Override
public Object visitLiteralExpression(JsonPathParser.LiteralExpressionContext ctx) {
String s = null;
if (ctx.StringLiteral() != null) {
s = ctx.StringLiteral().getText();
} else if (!ctx.children.isEmpty()) {
s = ctx.children.get(0).getText();
}
if (s != null && (s.startsWith("'") || s.startsWith("\""))) {
return s.substring(1, s.length() - 1);
}
return "null".equals(s) ? null : s;
}
@Override
public Object visitUnaryExpression(JsonPathParser.UnaryExpressionContext ctx) {
if (ctx.AT() != null) {
if (scope instanceof Hcl.Literal) {
if (ctx.Identifier() == null && ctx.StringLiteral() == null) {
return scope;
}
} else if (scope instanceof Hcl.Attribute) {
Hcl.Attribute attr = (Hcl.Attribute) scope;
if (ctx.Identifier() != null || ctx.StringLiteral() != null) {
String key = attr.getSimpleName();
String name = ctx.StringLiteral() != null ?
unquoteStringLiteral(ctx.StringLiteral().getText()) : ctx.Identifier().getText();
if (key.equals(name)) {
return attr;
}
}
scope = attr.getValue();
return getResultFromList(visitUnaryExpression(ctx));
} else if (scope instanceof Hcl.Block) {
Hcl.Block block = (Hcl.Block) scope;
for (Hcl body : block.getBody()) {
String name = ctx.StringLiteral() != null ?
unquoteStringLiteral(ctx.StringLiteral().getText()) : ctx.Identifier().getText();
if (block.getType() instanceof Hcl.Identifier && block.getType().getName().equals(name)) {
return block;
} else if (body instanceof Hcl.Attribute) {
Hcl.Attribute attr = (Hcl.Attribute) body;
if (ctx.Identifier() != null || ctx.StringLiteral() != null) {
String key = attr.getSimpleName();
if (key.equals(name)) {
return block;
}
}
}
}
} else if (scope instanceof List) {
List results = ((List) scope).stream()
.map(o -> {
scope = o;
return visitUnaryExpression(ctx);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
// Unwrap lists of results from visitUnaryExpression to match the position of the cursor.
List matches = new ArrayList<>();
for (Object result : results) {
if (result instanceof List) {
matches.addAll(((List) result));
} else {
matches.add(result);
}
}
return getResultFromList(matches);
}
} else if (ctx.jsonPath() != null) {
Object result = visit(ctx.jsonPath());
return getResultByKey(result, ctx.stop.getText());
}
return null;
}
@Override
public Object visitRegexExpression(JsonPathParser.RegexExpressionContext ctx) {
if (scope == null || scope instanceof List && ((List) scope).isEmpty()) {
return null;
}
Object rhs = ctx.REGEX().getText();
Object lhs = visitUnaryExpression(ctx.unaryExpression());
String operator = "=~";
if (lhs instanceof List) {
List matches = new ArrayList<>();
for (Object match : ((List) lhs)) {
Hcl result = getOperatorResult(match, operator, rhs);
if (result != null) {
matches.add(match);
}
}
return matches;
} else {
return getOperatorResult(lhs, operator, rhs);
}
}
// Checks if a string contains the specified substring (case-sensitive), or an array contains the specified element.
@Override
public Object visitContainsExpression(JsonPathParser.ContainsExpressionContext ctx) {
Object originalScope = scope;
if (ctx.children.get(0) instanceof JsonPathParser.UnaryExpressionContext) {
Object lhs = visitUnaryExpression(ctx.unaryExpression());
Object rhs = visitLiteralExpression(ctx.literalExpression());
if (lhs instanceof Hcl.Block && rhs != null) {
Hcl.Block block = (Hcl.Block) lhs;
String key = ctx.children.get(0).getChild(2).getText();
lhs = getResultByKey(block, key);
if (lhs instanceof Hcl.Attribute) {
Hcl.Attribute attr = (Hcl.Attribute) lhs;
if (attr.getValue() instanceof Hcl.Literal) {
Hcl.Literal literal = (Hcl.Literal) attr.getValue();
if (literal.getValue().toString().contains(String.valueOf(rhs))) {
return originalScope;
}
}
}
}
} else {
Object lhs = visitLiteralExpression(ctx.literalExpression());
Object rhs = visitUnaryExpression(ctx.unaryExpression());
if (rhs instanceof Hcl.Block && lhs != null) {
Hcl.Block block = (Hcl.Block) rhs;
String key = ctx.children.get(2).getChild(2).getText();
rhs = getResultByKey(block, key);
if (rhs instanceof Hcl.Attribute) {
Hcl.Attribute attr = (Hcl.Attribute) rhs;
if (attr.getValue() instanceof Hcl.Literal) {
Hcl.Literal literal = (Hcl.Literal) attr.getValue();
if (literal.getValue().toString().contains(String.valueOf(lhs))) {
return originalScope;
}
}
}
}
}
return null;
}
@Override
public Object visitBinaryExpression(JsonPathParser.BinaryExpressionContext ctx) {
Object lhs = ctx.children.get(0);
Object rhs = ctx.children.get(2);
if (ctx.LOGICAL_OPERATOR() != null) {
String operator;
switch (ctx.LOGICAL_OPERATOR().getText()) {
case ("&&"):
operator = "&&";
break;
case ("||"):
operator = "||";
break;
default:
return false;
}
Object scopeOfLogicalOp = scope;
lhs = getBinaryExpressionResult(lhs);
scope = scopeOfLogicalOp;
rhs = getBinaryExpressionResult(rhs);
if ("&&".equals(operator) &&
((lhs != null && (!(lhs instanceof List) || !((List) lhs).isEmpty())) && (rhs != null && (!(rhs instanceof List) || !((List) rhs).isEmpty())))) {
return scopeOfLogicalOp;
} else if ("||".equals(operator) &&
((lhs != null && (!(lhs instanceof List) || !((List) lhs).isEmpty())) || (rhs != null && (!(rhs instanceof List) || !((List) rhs).isEmpty())))) {
return scopeOfLogicalOp;
}
} else if (ctx.EQUALITY_OPERATOR() != null) {
// Equality operators may resolve the LHS and RHS without caching scope.
Object originalScope = scope;
lhs = getBinaryExpressionResult(lhs);
rhs = getBinaryExpressionResult(rhs);
String operator;
switch (ctx.EQUALITY_OPERATOR().getText()) {
case ("=="):
operator = "==";
break;
case ("!="):
operator = "!=";
break;
default:
return null;
}
if (lhs instanceof List) {
List matches = new ArrayList<>();
for (Object match : ((List) lhs)) {
Hcl result = getOperatorResult(match, operator, rhs);
if (result != null) {
matches.add(match);
}
}
return matches;
} else {
if (originalScope instanceof Hcl.Attribute) {
if (getOperatorResult(lhs, operator, rhs) != null) {
return originalScope;
}
} else {
return getOperatorResult(lhs, operator, rhs);
}
}
}
return null;
}
@Nullable
private Object getBinaryExpressionResult(Object ctx) {
if (ctx instanceof JsonPathParser.BinaryExpressionContext) {
ctx = visitBinaryExpression((JsonPathParser.BinaryExpressionContext) ctx);
} else if (ctx instanceof JsonPathParser.RegexExpressionContext) {
ctx = visitRegexExpression((JsonPathParser.RegexExpressionContext) ctx);
} else if (ctx instanceof JsonPathParser.ContainsExpressionContext) {
ctx = visitContainsExpression((JsonPathParser.ContainsExpressionContext) ctx);
} else if (ctx instanceof JsonPathParser.UnaryExpressionContext) {
ctx = visitUnaryExpression((JsonPathParser.UnaryExpressionContext) ctx);
} else if (ctx instanceof JsonPathParser.LiteralExpressionContext) {
ctx = visitLiteralExpression((JsonPathParser.LiteralExpressionContext) ctx);
}
return ctx;
}
// Interpret the LHS to check the appropriate value.
@Nullable
private Hcl getOperatorResult(Object lhs, String operator, Object rhs) {
if (lhs instanceof Hcl.Attribute) {
Hcl.Attribute attr = (Hcl.Attribute) lhs;
if (attr.getValue() instanceof Hcl.Literal) {
Hcl.Literal literal = (Hcl.Literal) attr.getValue();
if (checkObjectEquality(literal.getValue(), operator, rhs)) {
return attr;
}
} else if (attr.getValue() instanceof Hcl.VariableExpression) {
Hcl.VariableExpression variable = (Hcl.VariableExpression) attr.getValue();
if (checkObjectEquality(variable.getName().getName(), operator, rhs)) {
return attr;
}
}
} else if (lhs instanceof Hcl.Block) {
Hcl.Block block = (Hcl.Block) lhs;
for (Hcl body : block.getBody()) {
if (body instanceof Hcl.Attribute) {
Hcl.Attribute attr = (Hcl.Attribute) body;
if (attr.getValue() instanceof Hcl.Literal &&
checkObjectEquality(((Hcl.Literal) attr.getValue()).getValue(), operator, rhs)) {
return block;
}
}
}
} else if (lhs instanceof Hcl.Literal) {
Hcl.Literal literal = (Hcl.Literal) lhs;
if (checkObjectEquality(literal.getValue(), operator, rhs)) {
return literal;
}
}
return null;
}
private boolean checkObjectEquality(Object lhs, String operator, Object rhs) {
BiPredicate predicate = (lh, rh) -> {
switch (operator) {
case "==":
return Objects.equals(lh, rh);
case "!=":
return !Objects.equals(lh, rh);
case "=~":
return Pattern.compile(rh.toString()).matcher(lh.toString()).matches();
}
return false;
};
return predicate.test(lhs, rhs);
}
// Extract the result from JSON objects that can match by key.
@Nullable
public Object getResultByKey(Object result, String key) {
if (result instanceof Hcl.Block) {
Hcl.Block block = (Hcl.Block) result;
for (Hcl body : block.getBody()) {
if (body instanceof Hcl.Attribute) {
Hcl.Attribute attr = (Hcl.Attribute) body;
if (attr.getSimpleName().equals(key)) {
return attr;
}
}
}
} else if (result instanceof Hcl.Attribute) {
Hcl.Attribute attr = (Hcl.Attribute) result;
if (attr.getValue() instanceof Hcl.Literal) {
return attr.getSimpleName().equals(key) ? attr : null;
}
} else if (result instanceof List) {
for (Object o : ((List) result)) {
Object r = getResultByKey(o, key);
if (r != null) {
return r;
}
}
}
return null;
}
// Ensure the scope is set correctly when results are wrapped in a list.
private Object getResultFromList(Object results) {
if (results instanceof List) {
List matches = (List) results;
if (matches.isEmpty()) {
return null;
} else if (matches.size() == 1) {
return matches.get(0);
}
}
return results;
}
// Extract the value from a Json object.
@Nullable
private Object getValue(Object result) {
if (result instanceof Hcl.Attribute) {
return getValue(((Hcl.Attribute) result).getValue());
} else if (result instanceof Hcl.Block) {
return ((Hcl.Block) result).getBody();
} else if (result instanceof List) {
return ((List) result).stream()
.map(this::getValue)
.filter(Objects::nonNull)
.collect(Collectors.toList());
} else if (result instanceof Hcl.Literal) {
return ((Hcl.Literal) result).getValue();
} else if (result instanceof String) {
return result;
}
return null;
}
private static String unquoteStringLiteral(String literal) {
if (literal != null && (literal.startsWith("'") || literal.startsWith("\""))) {
return literal.substring(1, literal.length() - 1);
}
return "null".equals(literal) ? null : literal;
}
}
}