All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openrewrite.hcl.JsonPathMatcher Maven / Gradle / Ivy

There is a newer version: 8.40.2
Show newest version
/*
 * 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.jspecify.annotations.Nullable; 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 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 @Nullable 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 @Nullable 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 @Nullable 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 @Nullable 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 @Nullable 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 @Nullable 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 @Nullable 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 @Nullable 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; } private @Nullable 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. private @Nullable 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. public @Nullable 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. private @Nullable 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; } } }