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

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

The newest version!
/*
 *  Copyright 2021 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.yaml; 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.yaml.internal.grammar.JsonPathLexer; import org.openrewrite.yaml.internal.grammar.JsonPathParser; import org.openrewrite.yaml.internal.grammar.JsonPathParserBaseVisitor; import org.openrewrite.yaml.internal.grammar.JsonPathParserVisitor; import org.openrewrite.yaml.tree.Yaml; 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 specified 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) .map(t -> new ReplaceAliasWithAnchorValueVisitor() .visitNonNull(t, 0)) .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 JsonPathMatcher.JsonPathYamlVisitor(cursorPath, start, stop, false); Object result = v.visit(ctx); //noinspection unchecked return Optional.ofNullable((T) result); } public boolean matches(Cursor cursor) { Object cursorValue = new ReplaceAliasWithAnchorValueVisitor().visit((Tree)cursor.getValue(), 0); List cursorPath = cursor.getPathAsStream() .map(cp -> { if (cp instanceof Yaml) { cp = new ReplaceAliasWithAnchorValueVisitor().visit((Yaml) cp, 0); } return cp; }) .collect(Collectors.toList()); return find(cursor).map(o -> { if (o instanceof List) { //noinspection unchecked List l = (List) o; return !disjoint(l, cursorPath) && l.contains(cursorValue); } else { return Objects.equals(o, cursorValue); } }).orElse(false); } private JsonPathParser jsonPath() { return new JsonPathParser(new CommonTokenStream(new JsonPathLexer(CharStreams.fromString(this.jsonPath)))); } @SuppressWarnings({"ConstantConditions", "unchecked"}) private static class JsonPathYamlVisitor extends JsonPathParserBaseVisitor { private final List cursorPath; protected Object scope; private final JsonPathParser.ExpressionContext stop; private final boolean isRecursiveDescent; public JsonPathYamlVisitor(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 Yaml.Mapping) .findFirst() .orElseGet(() -> cursorPath.stream() .filter(t -> t instanceof Yaml.Document && ((Yaml.Document) t).getBlock() instanceof Yaml.Mapping) .map(t -> ((Yaml.Document) t).getBlock()) .findFirst() .orElse(null)); } return super.visitJsonPath(ctx); } private JsonPathParser.ExpressionContext getExpressionContext(ParserRuleContext ctx) { if (ctx == null || ctx instanceof JsonPathParser.ExpressionContext) { return (JsonPathParser.ExpressionContext) ctx; } return getExpressionContext(ctx.getParent()); } @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) { JsonPathMatcher.JsonPathYamlVisitor v = new JsonPathMatcher.JsonPathYamlVisitor(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 { JsonPathMatcher.JsonPathYamlVisitor v = new JsonPathMatcher.JsonPathYamlVisitor(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 Yaml.Sequence) { Yaml.Sequence array = (Yaml.Sequence) scope; results = new ArrayList<>(array.getEntries()); } else if (scope instanceof Yaml.Mapping.Entry) { scope = ((Yaml.Mapping.Entry) 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 Yaml.Sequence) { Yaml.Sequence array = (Yaml.Sequence) scope; results = new ArrayList<>(array.getEntries()); } else if (scope instanceof Yaml.Mapping.Entry) { scope = ((Yaml.Mapping.Entry) 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 Yaml.Mapping) { Yaml.Mapping mapping = (Yaml.Mapping) scope; if (isRecursiveDescent) { scope = mapping.getEntries(); Object result = getResultFromList(visitProperty(ctx)); return getResultFromList(result); } else { for (Yaml.Mapping.Entry entry : mapping.getEntries()) { if (entry instanceof Yaml.Mapping.Entry) { String key = entry.getKey().getValue(); String name = ctx.StringLiteral() != null ? unquoteStringLiteral(ctx.StringLiteral().getText()) : ctx.Identifier().getText(); if (key.equals(name)) { return entry; } } } } } else if (scope instanceof Yaml.Mapping.Entry) { Yaml.Mapping.Entry member = (Yaml.Mapping.Entry) scope; List matches = new ArrayList<>(); String key = member.getKey().getValue(); String name = ctx.StringLiteral() != null ? unquoteStringLiteral(ctx.StringLiteral().getText()) : ctx.Identifier().getText(); if (isRecursiveDescent) { if (key.equals(name)) { matches.add(member); } if (!(member.getValue() instanceof Yaml.Scalar)) { scope = member.getValue(); Object result = getResultFromList(visitProperty(ctx)); if (result != null) { matches.add(result); } } return getResultFromList(matches); } else if (((member.getValue() instanceof Yaml.Scalar))) { return key.equals(name) ? member : null; } scope = member.getValue(); return visitProperty(ctx); } else if (scope instanceof Yaml.Sequence) { Object matches = ((Yaml.Sequence) scope).getEntries().stream() .map(o -> { scope = o; return visitProperty(ctx); }) .filter(Objects::nonNull) .collect(Collectors.toList()); return getResultFromList(matches); } else if (scope instanceof Yaml.Sequence.Entry) { Yaml.Sequence.Entry entry = (Yaml.Sequence.Entry) scope; scope = entry.getBlock(); return visitProperty(ctx); } 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; } @Override public Object visitWildcard(JsonPathParser.WildcardContext ctx) { if (scope instanceof Yaml.Mapping) { Yaml.Mapping mapping = (Yaml.Mapping) scope; return mapping.getEntries(); } else if (scope instanceof Yaml.Mapping.Entry) { Yaml.Mapping.Entry member = (Yaml.Mapping.Entry) scope; return member.getValue(); } else if (scope instanceof Yaml.Sequence) { Object matches = ((Yaml.Sequence) scope).getEntries().stream() .map(o -> { scope = o; return visitWildcard(ctx); }) .filter(Objects::nonNull) .collect(Collectors.toList()); return getResultFromList(matches); } else if (scope instanceof Yaml.Sequence.Entry) { Yaml.Sequence.Entry entry = (Yaml.Sequence.Entry) scope; return entry.getBlock(); } 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); } 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 Yaml.Scalar) { if (ctx.Identifier() == null && ctx.StringLiteral() == null) { return scope; } } else if (scope instanceof Yaml.Mapping) { scope = ((Yaml.Mapping) scope).getEntries(); return visitUnaryExpression(ctx); } else if (scope instanceof Yaml.Mapping.Entry) { Yaml.Mapping.Entry entry = (Yaml.Mapping.Entry) scope; if (ctx.Identifier() != null || ctx.StringLiteral() != null) { String key = entry.getKey().getValue(); String name = ctx.StringLiteral() != null ? unquoteStringLiteral(ctx.StringLiteral().getText()) : ctx.Identifier().getText(); if (key.equals(name)) { return entry; } } scope = entry.getValue(); return getResultFromList(visitUnaryExpression(ctx)); } else if (scope instanceof Yaml.Sequence) { scope = ((Yaml.Sequence) scope).getEntries(); return visitUnaryExpression(ctx); } else if (scope instanceof Yaml.Sequence.Entry) { // Unary operators set the scope of the matched key within a block. Yaml.Sequence.Entry entry = (Yaml.Sequence.Entry) scope; scope = entry.getBlock(); Object result = visitUnaryExpression(ctx); if (result != null) { return getResultFromList(entry.getBlock()); } } 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)) { Yaml mappingOrEntry = getOperatorResult(match, operator, rhs); if (mappingOrEntry != null) { matches.add(mappingOrEntry); } } 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 List) { String key = ctx.children.get(0).getChild(2).getText(); lhs = getResultByKey(lhs, key); } if (lhs instanceof Yaml.Mapping.Entry && rhs != null) { Yaml.Mapping.Entry entry = (Yaml.Mapping.Entry) lhs; if (entry.getValue() instanceof Yaml.Sequence) { Yaml.Sequence sequence = (Yaml.Sequence) entry.getValue(); if (sequence.getEntries().stream() .filter(o -> o.getBlock() instanceof Yaml.Scalar) .map(o -> (Yaml.Scalar) o.getBlock()) .anyMatch(o -> o.getValue().contains(String.valueOf(rhs)))) { return originalScope; } } else if (entry.getValue() instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); if (scalar.getValue().contains(String.valueOf(rhs))) { return originalScope; } } } } else { Object lhs = visitLiteralExpression(ctx.literalExpression()); Object rhs = visitUnaryExpression(ctx.unaryExpression()); if (rhs instanceof List) { String key = ctx.children.get(2).getChild(2).getText(); rhs = getResultByKey(rhs, key); } if (rhs instanceof Yaml.Mapping.Entry && lhs != null) { Yaml.Mapping.Entry entry = (Yaml.Mapping.Entry) rhs; if (entry.getValue() instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); if (scalar.getValue().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 the result of the evaluated expression. if (lhs instanceof Yaml) { return rhs; } else if (rhs instanceof Yaml) { return lhs; } // Return the result of the expression that has the fewest matches. if (lhs instanceof List && rhs instanceof List && ((List) lhs).size() != ((List) rhs).size()) { return ((List) lhs).size() < ((List) rhs).size() ? lhs : rhs; } 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)) { Yaml result = getOperatorResult(match, operator, rhs); if (result != null) { matches.add(result); } } // The filterExpression matched a result inside a Mapping. I.E. originalScope[?(filterExpression)] if (originalScope instanceof Yaml.Mapping.Entry && ((Yaml.Mapping.Entry) originalScope).getValue() instanceof Yaml.Mapping) { return originalScope; } return matches; } else { if (originalScope instanceof Yaml.Mapping.Entry) { 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 Yaml getOperatorResult(Object lhs, String operator, Object rhs) { if (lhs instanceof Yaml.Mapping) { Yaml.Mapping mapping = (Yaml.Mapping) lhs; for (Yaml.Mapping.Entry entry : mapping.getEntries()) { if (entry.getValue() instanceof Yaml.Scalar && checkObjectEquality(((Yaml.Scalar) entry.getValue()).getValue(), operator, rhs)) { return mapping; } } } else if (lhs instanceof Yaml.Mapping.Entry) { Yaml.Mapping.Entry entry = (Yaml.Mapping.Entry) lhs; if (entry.getValue() instanceof Yaml.Scalar && checkObjectEquality(((Yaml.Scalar) entry.getValue()).getValue(), operator, rhs)) { return entry; } } else if (lhs instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) lhs; if (checkObjectEquality(scalar.getValue(), operator, rhs)) { return scalar; } } return null; } private boolean checkObjectEquality(Object lhs, String operator, Object rhs) { if (lhs == null || rhs == null) { return false; } 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 YAML objects that can match by key. public @Nullable Object getResultByKey(Object result, String key) { if (result instanceof Yaml.Mapping.Entry) { Yaml.Mapping.Entry member = (Yaml.Mapping.Entry) result; if (member.getValue() instanceof Yaml.Scalar) { return member.getKey().getValue().equals(key) ? member : 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 Yaml.Mapping.Entry) { return getValue(((Yaml.Mapping.Entry) result).getValue()); } else if (result instanceof Yaml.Mapping) { return ((Yaml.Mapping) result).getEntries(); } else if (result instanceof List) { return ((List) result).stream() .map(this::getValue) .filter(Objects::nonNull) .collect(Collectors.toList()); } else if (result instanceof Yaml.Sequence) { return ((Yaml.Sequence) result).getEntries(); } else if (result instanceof Yaml.Scalar) { return ((Yaml.Scalar) 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; } } }