From d3a30bc7cfb213b3b249e866afae7bf5924e35b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:23:49 +0000 Subject: [PATCH 1/5] Issue #128 Refactor JsonPath to sealed interface with AST interpreter Refactoring to prepare for runtime compilation support: - JsonPath is now a sealed interface permitting JsonPathInterpreted and JsonPathCompiled - JsonPathInterpreted contains the existing AST-walking evaluation logic - JsonPathCompiled stub added for future bytecode-compiled implementation - JsonPathCompiler stub added with in-memory compilation infrastructure - All 100 existing tests pass unchanged Next steps: Add tests and implementation for runtime-compiled JsonPath Co-authored-by: simbo1905 --- .../java/json/java21/jsonpath/JsonPath.java | 596 ++------------ .../java21/jsonpath/JsonPathCompiled.java | 51 ++ .../java21/jsonpath/JsonPathCompiler.java | 728 ++++++++++++++++++ .../java21/jsonpath/JsonPathInterpreted.java | 565 ++++++++++++++ 4 files changed, 1389 insertions(+), 551 deletions(-) create mode 100644 json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java create mode 100644 json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java create mode 100644 json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathInterpreted.java diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPath.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPath.java index a18225a..fc413de 100644 --- a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPath.java +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPath.java @@ -1,13 +1,12 @@ package json.java21.jsonpath; -import jdk.sandbox.java.util.json.*; +import jdk.sandbox.java.util.json.JsonValue; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.logging.Logger; -/// JsonPath query evaluator for JSON documents. +/// JsonPath query evaluator interface for JSON documents. /// Parses JsonPath expressions and evaluates them against JsonValue instances. /// /// Usage examples: @@ -19,30 +18,16 @@ /// // Compiled + static call site (also reusable) /// JsonPath path = JsonPath.parse("$.store.book[*].author"); /// List results = JsonPath.query(path, json); +/// +/// // Runtime-compiled for best performance +/// JsonPath compiled = JsonPath.compile(path); +/// List results = compiled.query(json); /// ``` /// /// Based on the JSONPath specification from [...](https://goessner.net/articles/JsonPath/) -public final class JsonPath { - - private static final Logger LOG = Logger.getLogger(JsonPath.class.getName()); +public sealed interface JsonPath permits JsonPathInterpreted, JsonPathCompiled { - private final JsonPathAst.Root ast; - - private JsonPath(JsonPathAst.Root ast) { - this.ast = ast; - } - - /// Parses a JsonPath expression and returns a compiled JsonPath for reuse. - /// @param path the JsonPath expression - /// @return a compiled JsonPath that can be used to select from multiple documents - /// @throws NullPointerException if path is null - /// @throws JsonPathParseException if the path is invalid - public static JsonPath parse(String path) { - Objects.requireNonNull(path, "path must not be null"); - LOG.fine(() -> "Parsing path: " + path); - final var ast = JsonPathParser.parse(path); - return new JsonPath(ast); - } + Logger LOG = Logger.getLogger(JsonPath.class.getName()); /// Queries matching values from a JSON document. /// @@ -52,16 +37,30 @@ public static JsonPath parse(String path) { /// @param json the JSON document to query /// @return a list of matching JsonValue instances (maybe empty) /// @throws NullPointerException if JSON is null - public List query(JsonValue json) { - Objects.requireNonNull(json, "json must not be null"); - LOG.fine(() -> "Querying document with path: " + this); - return evaluate(ast, json); + List query(JsonValue json); + + /// Parses a JsonPath expression and returns an interpreted JsonPath for reuse. + /// @param path the JsonPath expression + /// @return an interpreted JsonPath that can be used to select from multiple documents + /// @throws NullPointerException if path is null + /// @throws JsonPathParseException if the path is invalid + static JsonPath parse(String path) { + Objects.requireNonNull(path, "path must not be null"); + LOG.fine(() -> "Parsing path: " + path); + final var ast = JsonPathParser.parse(path); + return new JsonPathInterpreted(ast); } - /// Reconstructs the JsonPath expression from the AST. - @Override - public String toString() { - return reconstruct(ast); + /// Compiles a JsonPath into optimized bytecode for best performance. + /// If the input is already compiled, returns it unchanged. + /// @param path the JsonPath to compile (typically from `parse()`) + /// @return a compiled JsonPath with optimal performance + static JsonPath compile(JsonPath path) { + Objects.requireNonNull(path, "path must not be null"); + return switch (path) { + case JsonPathCompiled compiled -> compiled; + case JsonPathInterpreted interpreted -> JsonPathCompiler.compile(interpreted); + }; } /// Evaluates a compiled JsonPath against a JSON document. @@ -69,7 +68,7 @@ public String toString() { /// @param json the JSON document to query /// @return a list of matching JsonValue instances (maybe empty) /// @throws NullPointerException if path or JSON is null - public static List query(JsonPath path, JsonValue json) { + static List query(JsonPath path, JsonValue json) { Objects.requireNonNull(path, "path must not be null"); return path.query(json); } @@ -83,534 +82,29 @@ public static List query(JsonPath path, JsonValue json) { /// @return a list of matching JsonValue instances (maybe empty) /// @throws NullPointerException if path or JSON is null /// @throws JsonPathParseException if the path is invalid - public static List query(String path, JsonValue json) { + static List query(String path, JsonValue json) { Objects.requireNonNull(path, "path must not be null"); Objects.requireNonNull(json, "json must not be null"); return parse(path).query(json); } - /// Evaluates a pre-parsed JsonPath AST against a JSON document. - /// @param ast the parsed JsonPath AST - /// @param json the JSON document to query - /// @return a list of matching JsonValue instances (maybe empty) - static List evaluate(JsonPathAst.Root ast, JsonValue json) { - Objects.requireNonNull(ast, "ast must not be null"); - Objects.requireNonNull(json, "json must not be null"); - - final var results = new ArrayList(); - evaluateSegments(ast.segments(), 0, json, json, results); - return results; - } - - private static void evaluateSegments( - List segments, - int index, - JsonValue current, - JsonValue root, - List results) { - - // If we've processed all segments, current is a match - if (index >= segments.size()) { - results.add(current); - return; - } - - final var segment = segments.get(index); - LOG.finer(() -> "Evaluating segment " + index + ": " + segment); - - switch (segment) { - case JsonPathAst.PropertyAccess prop -> evaluatePropertyAccess(prop, segments, index, current, root, results); - case JsonPathAst.ArrayIndex arr -> evaluateArrayIndex(arr, segments, index, current, root, results); - case JsonPathAst.ArraySlice slice -> evaluateArraySlice(slice, segments, index, current, root, results); - case JsonPathAst.Wildcard ignored -> evaluateWildcard(segments, index, current, root, results); - case JsonPathAst.RecursiveDescent desc -> evaluateRecursiveDescent(desc, segments, index, current, root, results); - case JsonPathAst.Filter filter -> evaluateFilter(filter, segments, index, current, root, results); - case JsonPathAst.Union union -> evaluateUnion(union, segments, index, current, root, results); - case JsonPathAst.ScriptExpression script -> evaluateScriptExpression(script, segments, index, current, root, results); - } + /// Returns true if this JsonPath is an AST-based interpreter (not yet compiled). + default boolean isInterpreted() { + return this instanceof JsonPathInterpreted; } - private static void evaluatePropertyAccess( - JsonPathAst.PropertyAccess prop, - List segments, - int index, - JsonValue current, - JsonValue root, - List results) { - - if (current instanceof JsonObject obj) { - final var value = obj.members().get(prop.name()); - if (value != null) { - evaluateSegments(segments, index + 1, value, root, results); - } - } + /// Returns true if this JsonPath is compiled to bytecode. + default boolean isCompiled() { + return this instanceof JsonPathCompiled; } - private static void evaluateArrayIndex( - JsonPathAst.ArrayIndex arr, - List segments, - int index, - JsonValue current, - JsonValue root, - List results) { - - if (current instanceof JsonArray array) { - final var elements = array.elements(); - int idx = arr.index(); - - // Handle negative indices (from end) - if (idx < 0) { - idx = elements.size() + idx; - } - - if (idx >= 0 && idx < elements.size()) { - evaluateSegments(segments, index + 1, elements.get(idx), root, results); - } - } - } - - private static void evaluateArraySlice( - JsonPathAst.ArraySlice slice, - List segments, - int index, - JsonValue current, - JsonValue root, - List results) { - - if (current instanceof JsonArray array) { - final var elements = array.elements(); - final int size = elements.size(); - - final int step = slice.step() != null ? slice.step() : 1; - - if (step == 0) { - return; // Invalid step - } - - if (step > 0) { - int start = slice.start() != null ? normalizeIndex(slice.start(), size) : 0; - int end = slice.end() != null ? normalizeIndex(slice.end(), size) : size; - - start = Math.max(0, Math.min(start, size)); - end = Math.max(0, Math.min(end, size)); - - for (int i = start; i < end; i += step) { - evaluateSegments(segments, index + 1, elements.get(i), root, results); - } - } else { - int start = slice.start() != null ? normalizeIndex(slice.start(), size) : size - 1; - final int end = slice.end() != null ? normalizeIndex(slice.end(), size) : -1; - - start = Math.max(0, Math.min(start, size - 1)); - - for (int i = start; i > end; i += step) { - evaluateSegments(segments, index + 1, elements.get(i), root, results); - } - } - } - } - - private static int normalizeIndex(int index, int size) { - if (index < 0) { - return size + index; - } - return index; - } - - private static void evaluateWildcard( - List segments, - int index, - JsonValue current, - JsonValue root, - List results) { - - if (current instanceof JsonObject obj) { - for (final var value : obj.members().values()) { - evaluateSegments(segments, index + 1, value, root, results); - } - } else if (current instanceof JsonArray array) { - for (final var element : array.elements()) { - evaluateSegments(segments, index + 1, element, root, results); - } - } - } - - private static void evaluateRecursiveDescent( - JsonPathAst.RecursiveDescent desc, - List segments, - int index, - JsonValue current, - JsonValue root, - List results) { - - // First, try matching the target at current level - evaluateTargetSegment(desc.target(), segments, index, current, root, results); - - // Then recurse into children - if (current instanceof JsonObject obj) { - for (final var value : obj.members().values()) { - evaluateRecursiveDescent(desc, segments, index, value, root, results); - } - } else if (current instanceof JsonArray array) { - for (final var element : array.elements()) { - evaluateRecursiveDescent(desc, segments, index, element, root, results); - } - } - } - - private static void evaluateTargetSegment( - JsonPathAst.Segment target, - List segments, - int index, - JsonValue current, - JsonValue root, - List results) { - - switch (target) { - case JsonPathAst.PropertyAccess prop -> { - if (current instanceof JsonObject obj) { - final var value = obj.members().get(prop.name()); - if (value != null) { - evaluateSegments(segments, index + 1, value, root, results); - } - } - } - case JsonPathAst.Wildcard ignored -> { - if (current instanceof JsonObject obj) { - for (final var value : obj.members().values()) { - evaluateSegments(segments, index + 1, value, root, results); - } - } else if (current instanceof JsonArray array) { - for (final var element : array.elements()) { - evaluateSegments(segments, index + 1, element, root, results); - } - } - } - case JsonPathAst.ArrayIndex arr -> { - if (current instanceof JsonArray array) { - final var elements = array.elements(); - int idx = arr.index(); - if (idx < 0) idx = elements.size() + idx; - if (idx >= 0 && idx < elements.size()) { - evaluateSegments(segments, index + 1, elements.get(idx), root, results); - } - } - } - default -> // Other segment types in recursive descent context - LOG.finer(() -> "Unsupported target in recursive descent: " + target); - } - } - - private static void evaluateFilter( - JsonPathAst.Filter filter, - List segments, - int index, - JsonValue current, - JsonValue root, - List results) { - - if (current instanceof JsonArray array) { - for (final var element : array.elements()) { - if (matchesFilter(filter.expression(), element)) { - evaluateSegments(segments, index + 1, element, root, results); - } - } - } - } - - private static boolean matchesFilter(JsonPathAst.FilterExpression expr, JsonValue current) { - return switch (expr) { - case JsonPathAst.ExistsFilter exists -> { - final var value = resolvePropertyPath(exists.path(), current); - yield value != null; - } - case JsonPathAst.ComparisonFilter comp -> { - final var leftValue = resolveFilterExpression(comp.left(), current); - final var rightValue = resolveFilterExpression(comp.right(), current); - yield compareValues(leftValue, comp.op(), rightValue); - } - case JsonPathAst.LogicalFilter logical -> { - final var leftMatch = matchesFilter(logical.left(), current); - yield switch (logical.op()) { - case AND -> leftMatch && matchesFilter(logical.right(), current); - case OR -> leftMatch || matchesFilter(logical.right(), current); - case NOT -> !leftMatch; - }; - } - case JsonPathAst.CurrentNode ignored1 -> true; - case JsonPathAst.PropertyPath path -> resolvePropertyPath(path, current) != null; - case JsonPathAst.LiteralValue ignored -> true; + /// Returns the AST for this JsonPath, if available. + /// Compiled paths may not have the AST readily available. + /// @return the AST root, or null if not available + default JsonPathAst.Root ast() { + return switch (this) { + case JsonPathInterpreted interpreted -> interpreted.ast(); + case JsonPathCompiled compiled -> compiled.ast(); }; } - - private static Object resolveFilterExpression(JsonPathAst.FilterExpression expr, JsonValue current) { - return switch (expr) { - case JsonPathAst.PropertyPath path -> { - final var value = resolvePropertyPath(path, current); - yield jsonValueToComparable(value); - } - case JsonPathAst.LiteralValue lit -> lit.value(); - case JsonPathAst.CurrentNode ignored -> jsonValueToComparable(current); - default -> null; - }; - } - - private static JsonValue resolvePropertyPath(JsonPathAst.PropertyPath path, JsonValue current) { - JsonValue value = current; - for (final var prop : path.properties()) { - if (value instanceof JsonObject obj) { - value = obj.members().get(prop); - if (value == null) { - return null; - } - } else { - return null; - } - } - return value; - } - - private static Object jsonValueToComparable(JsonValue value) { - if (value == null) return null; - return switch (value) { - case JsonString s -> s.string(); - case JsonNumber n -> n.toDouble(); - case JsonBoolean b -> b.bool(); - case JsonNull ignored -> null; - default -> value; - }; - } - - @SuppressWarnings("unchecked") - private static boolean compareValues(Object left, JsonPathAst.ComparisonOp op, Object right) { - if (left == null || right == null) { - return switch (op) { - case EQ -> left == right; - case NE -> left != right; - default -> false; - }; - } - - // Try numeric comparison - switch (left) { - case Number leftNum when right instanceof Number rightNum -> { - final double l = leftNum.doubleValue(); - final double r = rightNum.doubleValue(); - return switch (op) { - case EQ -> l == r; - case NE -> l != r; - case LT -> l < r; - case LE -> l <= r; - case GT -> l > r; - case GE -> l >= r; - }; - } - - - // String comparison - case String ignored when right instanceof String -> { - @SuppressWarnings("rawtypes") final int cmp = ((Comparable) left).compareTo(right); - return switch (op) { - case EQ -> cmp == 0; - case NE -> cmp != 0; - case LT -> cmp < 0; - case LE -> cmp <= 0; - case GT -> cmp > 0; - case GE -> cmp >= 0; - }; - } - - - // Boolean comparison - case Boolean ignored when right instanceof Boolean -> { - return switch (op) { - case EQ -> left.equals(right); - case NE -> !left.equals(right); - default -> false; - }; - } - default -> { - } - } - - // Fallback equality - return switch (op) { - case EQ -> left.equals(right); - case NE -> !left.equals(right); - default -> false; - }; - } - - private static void evaluateUnion( - JsonPathAst.Union union, - List segments, - int index, - JsonValue current, - JsonValue root, - List results) { - - for (final var selector : union.selectors()) { - switch (selector) { - case JsonPathAst.ArrayIndex arr -> evaluateArrayIndex(arr, segments, index, current, root, results); - case JsonPathAst.PropertyAccess prop -> evaluatePropertyAccess(prop, segments, index, current, root, results); - default -> LOG.finer(() -> "Unsupported selector in union: " + selector); - } - } - } - - private static void evaluateScriptExpression( - JsonPathAst.ScriptExpression script, - List segments, - int index, - JsonValue current, - JsonValue root, - List results) { - - if (current instanceof JsonArray array) { - // Simple support for @.length-1 pattern - final var scriptText = script.script().trim(); - if (scriptText.equals("@.length-1")) { - final int lastIndex = array.elements().size() - 1; - if (lastIndex >= 0) { - evaluateSegments(segments, index + 1, array.elements().get(lastIndex), root, results); - } - } else { - LOG.warning(() -> "Unsupported script expression: " + scriptText); - } - } - } - - private static String reconstruct(JsonPathAst.Root root) { - final var sb = new StringBuilder("$"); - for (final var segment : root.segments()) { - appendSegment(sb, segment); - } - return sb.toString(); - } - - private static void appendSegment(StringBuilder sb, JsonPathAst.Segment segment) { - switch (segment) { - case JsonPathAst.PropertyAccess prop -> { - if (isSimpleIdentifier(prop.name())) { - sb.append(".").append(prop.name()); - } else { - sb.append("['").append(escape(prop.name())).append("']"); - } - } - case JsonPathAst.ArrayIndex arr -> sb.append("[").append(arr.index()).append("]"); - case JsonPathAst.ArraySlice slice -> { - sb.append("["); - if (slice.start() != null) sb.append(slice.start()); - sb.append(":"); - if (slice.end() != null) sb.append(slice.end()); - if (slice.step() != null) sb.append(":").append(slice.step()); - sb.append("]"); - } - case JsonPathAst.Wildcard ignored -> sb.append(".*"); - case JsonPathAst.RecursiveDescent desc -> { - sb.append(".."); - // RecursiveDescent target is usually PropertyAccess or Wildcard, - // but can be other things in theory. - // If target is PropertyAccess("foo"), append "foo". - // If target is Wildcard, append "*". - // Our AST structure wraps the target segment. - // We need to handle how it's appended. - // appendSegment prepends "." or "[" usually. - // But ".." replaces the dot. - // Let special case the target printing. - appendRecursiveTarget(sb, desc.target()); - } - case JsonPathAst.Filter filter -> { - sb.append("[?("); - appendFilterExpression(sb, filter.expression()); - sb.append(")]"); - } - case JsonPathAst.Union union -> { - sb.append("["); - final var selectors = union.selectors(); - for (int i = 0; i < selectors.size(); i++) { - if (i > 0) sb.append(","); - appendUnionSelector(sb, selectors.get(i)); - } - sb.append("]"); - } - case JsonPathAst.ScriptExpression script -> sb.append("[(").append(script.script()).append(")]"); - } - } - - private static void appendRecursiveTarget(StringBuilder sb, JsonPathAst.Segment target) { - if (target instanceof JsonPathAst.PropertyAccess(String name)) { - sb.append(name); // ..name - } else if (target instanceof JsonPathAst.Wildcard) { - sb.append("*"); // ..* - } else { - // Fallback for other types if they ever occur in recursive position - appendSegment(sb, target); - } - } - - private static void appendUnionSelector(StringBuilder sb, JsonPathAst.Segment selector) { - if (selector instanceof JsonPathAst.PropertyAccess(String name)) { - sb.append("'").append(escape(name)).append("'"); - } else if (selector instanceof JsonPathAst.ArrayIndex(int index)) { - sb.append(index); - } else { - // Fallback - appendSegment(sb, selector); - } - } - - private static void appendFilterExpression(StringBuilder sb, JsonPathAst.FilterExpression expr) { - switch (expr) { - case JsonPathAst.ExistsFilter exists -> appendFilterExpression(sb, exists.path()); // Should print the path - case JsonPathAst.ComparisonFilter comp -> { - appendFilterExpression(sb, comp.left()); - sb.append(comp.op().symbol()); - appendFilterExpression(sb, comp.right()); - } - case JsonPathAst.LogicalFilter logical -> { - if (logical.op() == JsonPathAst.LogicalOp.NOT) { - sb.append("!"); - appendFilterExpression(sb, logical.left()); - } else { - sb.append("("); - appendFilterExpression(sb, logical.left()); - sb.append(" ").append(logical.op().symbol()).append(" "); - appendFilterExpression(sb, logical.right()); - sb.append(")"); - } - } - case JsonPathAst.CurrentNode ignored -> sb.append("@"); - case JsonPathAst.PropertyPath path -> { - sb.append("@"); - for (String p : path.properties()) { - if (isSimpleIdentifier(p)) { - sb.append(".").append(p); - } else { - sb.append("['").append(escape(p)).append("']"); - } - } - } - case JsonPathAst.LiteralValue lit -> { - if (lit.value() instanceof String s) { - sb.append("'").append(escape(s)).append("'"); - } else { - sb.append(lit.value()); - } - } - } - } - - private static boolean isSimpleIdentifier(String name) { - if (name.isEmpty()) return false; - if (!Character.isJavaIdentifierStart(name.charAt(0))) return false; - for (int i = 1; i < name.length(); i++) { - if (!Character.isJavaIdentifierPart(name.charAt(i))) return false; - } - return true; - } - - private static String escape(String s) { - return s.replace("'", "\\'"); - } } diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java new file mode 100644 index 0000000..825b384 --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java @@ -0,0 +1,51 @@ +package json.java21.jsonpath; + +import jdk.sandbox.java.util.json.JsonValue; + +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +/// Runtime-compiled implementation of JsonPath for optimal performance. +/// Uses JDK compiler tools to generate and compile Java bytecode at runtime. +final class JsonPathCompiled implements JsonPath { + + private static final Logger LOG = Logger.getLogger(JsonPathCompiled.class.getName()); + + private final JsonPathAst.Root astRoot; + private final JsonPathExecutor executor; + private final String originalPath; + + JsonPathCompiled(JsonPathAst.Root ast, JsonPathExecutor executor, String originalPath) { + this.astRoot = Objects.requireNonNull(ast, "ast must not be null"); + this.executor = Objects.requireNonNull(executor, "executor must not be null"); + this.originalPath = Objects.requireNonNull(originalPath, "originalPath must not be null"); + } + + @Override + public JsonPathAst.Root ast() { + return astRoot; + } + + @Override + public List query(JsonValue json) { + Objects.requireNonNull(json, "json must not be null"); + LOG.fine(() -> "Querying document with compiled path: " + this); + return executor.execute(json, json); + } + + @Override + public String toString() { + return originalPath; + } + + /// Functional interface for compiled JsonPath executors. + @FunctionalInterface + interface JsonPathExecutor { + /// Executes the compiled JsonPath query against a JSON document. + /// @param current the current node being evaluated + /// @param root the root document (for $ references in filters) + /// @return a list of matching JsonValue instances + List execute(JsonValue current, JsonValue root); + } +} diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java new file mode 100644 index 0000000..7802b7e --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java @@ -0,0 +1,728 @@ +package json.java21.jsonpath; + +import javax.tools.*; +import java.io.*; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.logging.Logger; + +/// Compiles JsonPath AST into optimized bytecode using JDK compiler tools. +/// Uses the Java Compiler API (javax.tools) to generate and compile Java source at runtime. +final class JsonPathCompiler { + + private static final Logger LOG = Logger.getLogger(JsonPathCompiler.class.getName()); + + /// Counter for generating unique class names + private static long classCounter = 0; + + private JsonPathCompiler() { + // utility class + } + + /// Compiles an interpreted JsonPath into an optimized compiled version. + /// @param interpreted the AST-based JsonPath to compile + /// @return a compiled JsonPath with generated bytecode + static JsonPathCompiled compile(JsonPathInterpreted interpreted) { + Objects.requireNonNull(interpreted, "interpreted must not be null"); + LOG.fine(() -> "Compiling JsonPath: " + interpreted); + + final var ast = interpreted.ast(); + final var originalPath = interpreted.toString(); + + // Generate a unique class name + final var className = generateClassName(); + LOG.finer(() -> "Generated class name: " + className); + + // Generate the Java source code + final var sourceCode = generateSourceCode(className, ast); + LOG.finer(() -> "Generated source code:\n" + sourceCode); + + // Compile the source code + final var executor = compileAndInstantiate(className, sourceCode); + + return new JsonPathCompiled(ast, executor, originalPath); + } + + /// Generates a unique class name for the compiled JsonPath executor. + private static synchronized String generateClassName() { + return "JsonPathExecutor_" + (classCounter++); + } + + /// Generates Java source code that implements the JsonPath query. + static String generateSourceCode(String className, JsonPathAst.Root ast) { + final var sb = new StringBuilder(); + + // Package and imports + sb.append(""" + package json.java21.jsonpath.generated; + + import jdk.sandbox.java.util.json.*; + import json.java21.jsonpath.JsonPathCompiled.JsonPathExecutor; + import java.util.*; + + public final class %s implements JsonPathExecutor { + + @Override + public List execute(JsonValue current, JsonValue root) { + final var results = new ArrayList(); + """.formatted(className)); + + // Generate the evaluation code + generateSegmentEvaluation(sb, ast.segments(), 0, "current", "root", "results"); + + // Close the execute method and add helper methods + sb.append(""" + return results; + } + + // Helper methods for generated code + + private static int normalizeIdx(int index, int size) { + return index < 0 ? size + index : index; + } + + private static JsonValue getPath(JsonValue current, String... props) { + JsonValue value = current; + for (String prop : props) { + if (value instanceof JsonObject obj) { + value = obj.members().get(prop); + if (value == null) return null; + } else { + return null; + } + } + return value; + } + + private static Object toComparable(JsonValue value) { + if (value == null) return null; + if (value instanceof JsonString s) return s.string(); + if (value instanceof JsonNumber n) return n.toDouble(); + if (value instanceof JsonBoolean b) return b.bool(); + if (value instanceof JsonNull) return null; + return value; + } + + private static boolean compareValues(Object left, String op, Object right) { + if (left == null || right == null) { + return switch (op) { + case "EQ" -> left == right; + case "NE" -> left != right; + default -> false; + }; + } + + if (left instanceof Number ln && right instanceof Number rn) { + double l = ln.doubleValue(); + double r = rn.doubleValue(); + return switch (op) { + case "EQ" -> l == r; + case "NE" -> l != r; + case "LT" -> l < r; + case "LE" -> l <= r; + case "GT" -> l > r; + case "GE" -> l >= r; + default -> false; + }; + } + + if (left instanceof String && right instanceof String) { + @SuppressWarnings("unchecked") + int cmp = ((Comparable) left).compareTo(right); + return switch (op) { + case "EQ" -> cmp == 0; + case "NE" -> cmp != 0; + case "LT" -> cmp < 0; + case "LE" -> cmp <= 0; + case "GT" -> cmp > 0; + case "GE" -> cmp >= 0; + default -> false; + }; + } + + if (left instanceof Boolean && right instanceof Boolean) { + return switch (op) { + case "EQ" -> left.equals(right); + case "NE" -> !left.equals(right); + default -> false; + }; + } + + return switch (op) { + case "EQ" -> left.equals(right); + case "NE" -> !left.equals(right); + default -> false; + }; + } + } + """); + + return sb.toString(); + } + + /// Generates evaluation code for a sequence of segments. + private static void generateSegmentEvaluation( + StringBuilder sb, + List segments, + int segmentIndex, + String currentVar, + String rootVar, + String resultsVar) { + + // If we've processed all segments, add current to results + if (segmentIndex >= segments.size()) { + sb.append(" %s.add(%s);\n".formatted(resultsVar, currentVar)); + return; + } + + final var segment = segments.get(segmentIndex); + final var nextIndex = segmentIndex + 1; + + switch (segment) { + case JsonPathAst.PropertyAccess prop -> + generatePropertyAccess(sb, prop, segments, nextIndex, currentVar, rootVar, resultsVar); + + case JsonPathAst.ArrayIndex arr -> + generateArrayIndex(sb, arr, segments, nextIndex, currentVar, rootVar, resultsVar); + + case JsonPathAst.ArraySlice slice -> + generateArraySlice(sb, slice, segments, nextIndex, currentVar, rootVar, resultsVar); + + case JsonPathAst.Wildcard ignored -> + generateWildcard(sb, segments, nextIndex, currentVar, rootVar, resultsVar); + + case JsonPathAst.RecursiveDescent desc -> + generateRecursiveDescent(sb, desc, segments, segmentIndex, currentVar, rootVar, resultsVar); + + case JsonPathAst.Filter filter -> + generateFilter(sb, filter, segments, nextIndex, currentVar, rootVar, resultsVar); + + case JsonPathAst.Union union -> + generateUnion(sb, union, segments, nextIndex, currentVar, rootVar, resultsVar); + + case JsonPathAst.ScriptExpression script -> + generateScriptExpression(sb, script, segments, nextIndex, currentVar, rootVar, resultsVar); + } + } + + private static void generatePropertyAccess( + StringBuilder sb, + JsonPathAst.PropertyAccess prop, + List segments, + int nextIndex, + String currentVar, + String rootVar, + String resultsVar) { + + final var tempVar = "v" + nextIndex; + sb.append(" if (%s instanceof JsonObject obj%d) {\n".formatted(currentVar, nextIndex)); + sb.append(" final var %s = obj%d.members().get(\"%s\");\n" + .formatted(tempVar, nextIndex, escapeJavaString(prop.name()))); + sb.append(" if (%s != null) {\n".formatted(tempVar)); + generateSegmentEvaluation(sb, segments, nextIndex, tempVar, rootVar, resultsVar); + sb.append(" }\n"); + sb.append(" }\n"); + } + + private static void generateArrayIndex( + StringBuilder sb, + JsonPathAst.ArrayIndex arr, + List segments, + int nextIndex, + String currentVar, + String rootVar, + String resultsVar) { + + final var tempVar = "e" + nextIndex; + final var idxVar = "idx" + nextIndex; + sb.append(" if (%s instanceof JsonArray arr%d) {\n".formatted(currentVar, nextIndex)); + sb.append(" final var elements%d = arr%d.elements();\n".formatted(nextIndex, nextIndex)); + sb.append(" int %s = %d;\n".formatted(idxVar, arr.index())); + sb.append(" if (%s < 0) %s = elements%d.size() + %s;\n".formatted(idxVar, idxVar, nextIndex, idxVar)); + sb.append(" if (%s >= 0 && %s < elements%d.size()) {\n".formatted(idxVar, idxVar, nextIndex)); + sb.append(" final var %s = elements%d.get(%s);\n".formatted(tempVar, nextIndex, idxVar)); + generateSegmentEvaluation(sb, segments, nextIndex, tempVar, rootVar, resultsVar); + sb.append(" }\n"); + sb.append(" }\n"); + } + + private static void generateArraySlice( + StringBuilder sb, + JsonPathAst.ArraySlice slice, + List segments, + int nextIndex, + String currentVar, + String rootVar, + String resultsVar) { + + sb.append(" if (%s instanceof JsonArray arr%d) {\n".formatted(currentVar, nextIndex)); + sb.append(" final var elements%d = arr%d.elements();\n".formatted(nextIndex, nextIndex)); + sb.append(" final int size%d = elements%d.size();\n".formatted(nextIndex, nextIndex)); + + final int step = slice.step() != null ? slice.step() : 1; + sb.append(" final int step%d = %d;\n".formatted(nextIndex, step)); + + if (step > 0) { + final var startExpr = slice.start() != null ? + "normalizeIdx(%d, size%d)".formatted(slice.start(), nextIndex) : "0"; + final var endExpr = slice.end() != null ? + "normalizeIdx(%d, size%d)".formatted(slice.end(), nextIndex) : "size" + nextIndex; + + sb.append(" int start%d = %s;\n".formatted(nextIndex, startExpr)); + sb.append(" int end%d = %s;\n".formatted(nextIndex, endExpr)); + sb.append(" start%d = Math.max(0, Math.min(start%d, size%d));\n".formatted(nextIndex, nextIndex, nextIndex)); + sb.append(" end%d = Math.max(0, Math.min(end%d, size%d));\n".formatted(nextIndex, nextIndex, nextIndex)); + sb.append(" for (int i%d = start%d; i%d < end%d; i%d += step%d) {\n" + .formatted(nextIndex, nextIndex, nextIndex, nextIndex, nextIndex, nextIndex)); + } else { + final var startExpr = slice.start() != null ? + "normalizeIdx(%d, size%d)".formatted(slice.start(), nextIndex) : "size%d - 1".formatted(nextIndex); + final var endExpr = slice.end() != null ? + "normalizeIdx(%d, size%d)".formatted(slice.end(), nextIndex) : "-1"; + + sb.append(" int start%d = %s;\n".formatted(nextIndex, startExpr)); + sb.append(" int end%d = %s;\n".formatted(nextIndex, endExpr)); + sb.append(" start%d = Math.max(0, Math.min(start%d, size%d - 1));\n".formatted(nextIndex, nextIndex, nextIndex)); + sb.append(" for (int i%d = start%d; i%d > end%d; i%d += step%d) {\n" + .formatted(nextIndex, nextIndex, nextIndex, nextIndex, nextIndex, nextIndex)); + } + + final var elemVar = "elem" + nextIndex; + sb.append(" final var %s = elements%d.get(i%d);\n".formatted(elemVar, nextIndex, nextIndex)); + generateSegmentEvaluation(sb, segments, nextIndex, elemVar, rootVar, resultsVar); + sb.append(" }\n"); + sb.append(" }\n"); + } + + private static void generateWildcard( + StringBuilder sb, + List segments, + int nextIndex, + String currentVar, + String rootVar, + String resultsVar) { + + final var elemVar = "wc" + nextIndex; + sb.append(" if (%s instanceof JsonObject wobj%d) {\n".formatted(currentVar, nextIndex)); + sb.append(" for (final var %s : wobj%d.members().values()) {\n".formatted(elemVar, nextIndex)); + generateSegmentEvaluation(sb, segments, nextIndex, elemVar, rootVar, resultsVar); + sb.append(" }\n"); + sb.append(" } else if (%s instanceof JsonArray warr%d) {\n".formatted(currentVar, nextIndex)); + sb.append(" for (final var %s : warr%d.elements()) {\n".formatted(elemVar, nextIndex)); + generateSegmentEvaluation(sb, segments, nextIndex, elemVar, rootVar, resultsVar); + sb.append(" }\n"); + sb.append(" }\n"); + } + + private static void generateRecursiveDescent( + StringBuilder sb, + JsonPathAst.RecursiveDescent desc, + List segments, + int segmentIndex, + String currentVar, + String rootVar, + String resultsVar) { + + // For recursive descent, we generate a helper method call + // For now, fall back to interpreter for complex recursive descent + sb.append(" // Recursive descent - delegating to interpreter\n"); + sb.append(" json.java21.jsonpath.JsonPathInterpreted.evaluateRecursiveDescent(\n"); + sb.append(" new json.java21.jsonpath.JsonPathAst.RecursiveDescent("); + generateAstLiteral(sb, desc.target()); + sb.append("),\n"); + sb.append(" java.util.List.of("); + for (int i = segmentIndex; i < segments.size(); i++) { + if (i > segmentIndex) sb.append(", "); + generateAstLiteral(sb, segments.get(i)); + } + sb.append("),\n"); + sb.append(" %d, %s, %s, %s);\n".formatted(segmentIndex, currentVar, rootVar, resultsVar)); + } + + private static void generateFilter( + StringBuilder sb, + JsonPathAst.Filter filter, + List segments, + int nextIndex, + String currentVar, + String rootVar, + String resultsVar) { + + final var elemVar = "fe" + nextIndex; + sb.append(" if (%s instanceof JsonArray farr%d) {\n".formatted(currentVar, nextIndex)); + sb.append(" for (final var %s : farr%d.elements()) {\n".formatted(elemVar, nextIndex)); + sb.append(" if ("); + generateFilterCondition(sb, filter.expression(), elemVar); + sb.append(") {\n"); + generateSegmentEvaluation(sb, segments, nextIndex, elemVar, rootVar, resultsVar); + sb.append(" }\n"); + sb.append(" }\n"); + sb.append(" }\n"); + } + + private static void generateFilterCondition(StringBuilder sb, JsonPathAst.FilterExpression expr, String currentVar) { + switch (expr) { + case JsonPathAst.ExistsFilter exists -> { + generatePropertyPathAccess(sb, exists.path(), currentVar); + sb.append(" != null"); + } + case JsonPathAst.ComparisonFilter comp -> { + sb.append("compareValues("); + generateFilterValue(sb, comp.left(), currentVar); + sb.append(", \"").append(comp.op().name()).append("\", "); + generateFilterValue(sb, comp.right(), currentVar); + sb.append(")"); + } + case JsonPathAst.LogicalFilter logical -> { + switch (logical.op()) { + case AND -> { + sb.append("("); + generateFilterCondition(sb, logical.left(), currentVar); + sb.append(" && "); + generateFilterCondition(sb, logical.right(), currentVar); + sb.append(")"); + } + case OR -> { + sb.append("("); + generateFilterCondition(sb, logical.left(), currentVar); + sb.append(" || "); + generateFilterCondition(sb, logical.right(), currentVar); + sb.append(")"); + } + case NOT -> { + sb.append("!("); + generateFilterCondition(sb, logical.left(), currentVar); + sb.append(")"); + } + } + } + case JsonPathAst.CurrentNode ignored -> sb.append("true"); + case JsonPathAst.PropertyPath path -> { + generatePropertyPathAccess(sb, path, currentVar); + sb.append(" != null"); + } + case JsonPathAst.LiteralValue ignored -> sb.append("true"); + } + } + + private static void generatePropertyPathAccess(StringBuilder sb, JsonPathAst.PropertyPath path, String currentVar) { + sb.append("getPath(%s".formatted(currentVar)); + for (final var prop : path.properties()) { + sb.append(", \"").append(escapeJavaString(prop)).append("\""); + } + sb.append(")"); + } + + private static void generateFilterValue(StringBuilder sb, JsonPathAst.FilterExpression expr, String currentVar) { + switch (expr) { + case JsonPathAst.PropertyPath path -> { + sb.append("toComparable("); + generatePropertyPathAccess(sb, path, currentVar); + sb.append(")"); + } + case JsonPathAst.LiteralValue lit -> { + if (lit.value() == null) { + sb.append("null"); + } else if (lit.value() instanceof String s) { + sb.append("\"").append(escapeJavaString(s)).append("\""); + } else if (lit.value() instanceof Number n) { + sb.append(n.doubleValue()); + } else if (lit.value() instanceof Boolean b) { + sb.append(b); + } else { + sb.append("null"); + } + } + case JsonPathAst.CurrentNode ignored -> + sb.append("toComparable(%s)".formatted(currentVar)); + default -> sb.append("null"); + } + } + + private static void generateUnion( + StringBuilder sb, + JsonPathAst.Union union, + List segments, + int nextIndex, + String currentVar, + String rootVar, + String resultsVar) { + + for (final var selector : union.selectors()) { + switch (selector) { + case JsonPathAst.ArrayIndex arr -> + generateArrayIndex(sb, arr, segments, nextIndex, currentVar, rootVar, resultsVar); + case JsonPathAst.PropertyAccess prop -> + generatePropertyAccess(sb, prop, segments, nextIndex, currentVar, rootVar, resultsVar); + default -> { + // Unsupported selector, skip + } + } + } + } + + private static void generateScriptExpression( + StringBuilder sb, + JsonPathAst.ScriptExpression script, + List segments, + int nextIndex, + String currentVar, + String rootVar, + String resultsVar) { + + // Only support @.length-1 pattern for now + if (script.script().trim().equals("@.length-1")) { + sb.append(" if (%s instanceof JsonArray sarr%d) {\n".formatted(currentVar, nextIndex)); + sb.append(" final int lastIdx%d = sarr%d.elements().size() - 1;\n".formatted(nextIndex, nextIndex)); + sb.append(" if (lastIdx%d >= 0) {\n".formatted(nextIndex)); + final var elemVar = "se" + nextIndex; + sb.append(" final var %s = sarr%d.elements().get(lastIdx%d);\n".formatted(elemVar, nextIndex, nextIndex)); + generateSegmentEvaluation(sb, segments, nextIndex, elemVar, rootVar, resultsVar); + sb.append(" }\n"); + sb.append(" }\n"); + } else { + // Unsupported script, delegate to interpreter + sb.append(" // Unsupported script expression - delegating to interpreter\n"); + sb.append(" json.java21.jsonpath.JsonPathInterpreted.evaluateScriptExpression(\n"); + sb.append(" new json.java21.jsonpath.JsonPathAst.ScriptExpression(\""); + sb.append(escapeJavaString(script.script())); + sb.append("\"),\n"); + sb.append(" java.util.List.of("); + for (int i = nextIndex - 1; i < segments.size(); i++) { + if (i > nextIndex - 1) sb.append(", "); + generateAstLiteral(sb, segments.get(i)); + } + sb.append("),\n"); + sb.append(" %d, %s, %s, %s);\n".formatted(nextIndex - 1, currentVar, rootVar, resultsVar)); + } + } + + private static void generateAstLiteral(StringBuilder sb, JsonPathAst.Segment segment) { + switch (segment) { + case JsonPathAst.PropertyAccess prop -> + sb.append("new json.java21.jsonpath.JsonPathAst.PropertyAccess(\"") + .append(escapeJavaString(prop.name())).append("\")"); + case JsonPathAst.ArrayIndex arr -> + sb.append("new json.java21.jsonpath.JsonPathAst.ArrayIndex(").append(arr.index()).append(")"); + case JsonPathAst.Wildcard ignored -> + sb.append("new json.java21.jsonpath.JsonPathAst.Wildcard()"); + case JsonPathAst.ArraySlice slice -> { + sb.append("new json.java21.jsonpath.JsonPathAst.ArraySlice("); + sb.append(slice.start() == null ? "null" : slice.start()).append(", "); + sb.append(slice.end() == null ? "null" : slice.end()).append(", "); + sb.append(slice.step() == null ? "null" : slice.step()).append(")"); + } + case JsonPathAst.RecursiveDescent desc -> { + sb.append("new json.java21.jsonpath.JsonPathAst.RecursiveDescent("); + generateAstLiteral(sb, desc.target()); + sb.append(")"); + } + case JsonPathAst.Filter filter -> { + sb.append("new json.java21.jsonpath.JsonPathAst.Filter("); + generateFilterExprLiteral(sb, filter.expression()); + sb.append(")"); + } + case JsonPathAst.Union union -> { + sb.append("new json.java21.jsonpath.JsonPathAst.Union(java.util.List.of("); + final var selectors = union.selectors(); + for (int i = 0; i < selectors.size(); i++) { + if (i > 0) sb.append(", "); + generateAstLiteral(sb, selectors.get(i)); + } + sb.append("))"); + } + case JsonPathAst.ScriptExpression script -> + sb.append("new json.java21.jsonpath.JsonPathAst.ScriptExpression(\"") + .append(escapeJavaString(script.script())).append("\")"); + } + } + + private static void generateFilterExprLiteral(StringBuilder sb, JsonPathAst.FilterExpression expr) { + switch (expr) { + case JsonPathAst.ExistsFilter exists -> { + sb.append("new json.java21.jsonpath.JsonPathAst.ExistsFilter("); + generateFilterExprLiteral(sb, exists.path()); + sb.append(")"); + } + case JsonPathAst.ComparisonFilter comp -> { + sb.append("new json.java21.jsonpath.JsonPathAst.ComparisonFilter("); + generateFilterExprLiteral(sb, comp.left()); + sb.append(", json.java21.jsonpath.JsonPathAst.ComparisonOp.").append(comp.op().name()).append(", "); + generateFilterExprLiteral(sb, comp.right()); + sb.append(")"); + } + case JsonPathAst.LogicalFilter logical -> { + sb.append("new json.java21.jsonpath.JsonPathAst.LogicalFilter("); + generateFilterExprLiteral(sb, logical.left()); + sb.append(", json.java21.jsonpath.JsonPathAst.LogicalOp.").append(logical.op().name()).append(", "); + if (logical.right() != null) { + generateFilterExprLiteral(sb, logical.right()); + } else { + sb.append("null"); + } + sb.append(")"); + } + case JsonPathAst.CurrentNode ignored -> + sb.append("new json.java21.jsonpath.JsonPathAst.CurrentNode()"); + case JsonPathAst.PropertyPath path -> { + sb.append("new json.java21.jsonpath.JsonPathAst.PropertyPath(java.util.List.of("); + for (int i = 0; i < path.properties().size(); i++) { + if (i > 0) sb.append(", "); + sb.append("\"").append(escapeJavaString(path.properties().get(i))).append("\""); + } + sb.append("))"); + } + case JsonPathAst.LiteralValue lit -> { + sb.append("new json.java21.jsonpath.JsonPathAst.LiteralValue("); + if (lit.value() == null) { + sb.append("null"); + } else if (lit.value() instanceof String s) { + sb.append("\"").append(escapeJavaString(s)).append("\""); + } else if (lit.value() instanceof Number n) { + if (lit.value() instanceof Long l) { + sb.append(l).append("L"); + } else { + sb.append(n.doubleValue()); + } + } else if (lit.value() instanceof Boolean b) { + sb.append(b); + } + sb.append(")"); + } + } + } + + /// Compiles the generated source code and instantiates the executor. + private static JsonPathCompiled.JsonPathExecutor compileAndInstantiate(String className, String sourceCode) { + final var compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + LOG.warning(() -> "No Java compiler available - falling back to interpreter delegation"); + throw new RuntimeException("Java compiler not available. Ensure you're running with a JDK, not a JRE."); + } + + final var fileManager = new InMemoryFileManager(compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)); + + // Create a source file object + final var fullClassName = "json.java21.jsonpath.generated." + className; + final var sourceFile = new InMemoryJavaFileObject(fullClassName, sourceCode); + + // Compile the source + final var diagnostics = new DiagnosticCollector(); + final var compilationUnits = Collections.singletonList(sourceFile); + final var options = List.of( + "-classpath", System.getProperty("java.class.path"), + "--add-exports", "json.java21.jsonpath/json.java21.jsonpath=ALL-UNNAMED", + "--add-exports", "json.java21/jdk.sandbox.java.util.json=ALL-UNNAMED" + ); + + final var task = compiler.getTask(null, fileManager, diagnostics, options, null, compilationUnits); + final boolean success = task.call(); + + if (!success) { + final var errors = new StringBuilder("Compilation failed:\n"); + for (final var diagnostic : diagnostics.getDiagnostics()) { + errors.append(diagnostic.toString()).append("\n"); + } + errors.append("\nSource code:\n").append(sourceCode); + LOG.severe(() -> "ERROR: " + errors); + throw new RuntimeException(errors.toString()); + } + + // Load and instantiate the compiled class + try { + final var classLoader = new InMemoryClassLoader(fileManager.getClassBytes()); + final var clazz = classLoader.loadClass(fullClassName); + return (JsonPathCompiled.JsonPathExecutor) clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException("Failed to instantiate compiled JsonPath executor", e); + } + } + + private static String escapeJavaString(String s) { + return s.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + /// In-memory Java source file object for compilation. + private static final class InMemoryJavaFileObject extends SimpleJavaFileObject { + private final String code; + + InMemoryJavaFileObject(String className, String code) { + super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return code; + } + } + + /// In-memory class file object for capturing compiled bytecode. + private static final class InMemoryClassFileObject extends SimpleJavaFileObject { + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + InMemoryClassFileObject(String className) { + super(URI.create("mem:///" + className.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS); + } + + @Override + public OutputStream openOutputStream() { + return outputStream; + } + + byte[] getBytes() { + return outputStream.toByteArray(); + } + } + + /// In-memory file manager that captures compiled class files. + private static final class InMemoryFileManager extends ForwardingJavaFileManager { + private final Map classFiles = new HashMap<>(); + + InMemoryFileManager(StandardJavaFileManager fileManager) { + super(fileManager); + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) { + if (kind == JavaFileObject.Kind.CLASS) { + final var classFile = new InMemoryClassFileObject(className); + classFiles.put(className, classFile); + return classFile; + } + return null; + } + + Map getClassBytes() { + final var result = new HashMap(); + for (final var entry : classFiles.entrySet()) { + result.put(entry.getKey(), entry.getValue().getBytes()); + } + return result; + } + } + + /// Class loader that loads compiled classes from memory. + private static final class InMemoryClassLoader extends ClassLoader { + private final Map classBytes; + + InMemoryClassLoader(Map classBytes) { + super(JsonPathCompiler.class.getClassLoader()); + this.classBytes = classBytes; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + final var bytes = classBytes.get(name); + if (bytes != null) { + return defineClass(name, bytes, 0, bytes.length); + } + return super.findClass(name); + } + } +} diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathInterpreted.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathInterpreted.java new file mode 100644 index 0000000..b2db283 --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathInterpreted.java @@ -0,0 +1,565 @@ +package json.java21.jsonpath; + +import jdk.sandbox.java.util.json.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +/// AST-walking interpreter implementation of JsonPath. +/// Parses JsonPath expressions and evaluates them against JsonValue instances by walking the AST. +final class JsonPathInterpreted implements JsonPath { + + private static final Logger LOG = Logger.getLogger(JsonPathInterpreted.class.getName()); + + private final JsonPathAst.Root astRoot; + + JsonPathInterpreted(JsonPathAst.Root ast) { + this.astRoot = Objects.requireNonNull(ast, "ast must not be null"); + } + + /// Returns the AST for this interpreted JsonPath. + @Override + public JsonPathAst.Root ast() { + return astRoot; + } + + @Override + public List query(JsonValue json) { + Objects.requireNonNull(json, "json must not be null"); + LOG.fine(() -> "Querying document with path: " + this); + return evaluate(astRoot, json); + } + + /// Reconstructs the JsonPath expression from the AST. + @Override + public String toString() { + return reconstruct(astRoot); + } + + /// Evaluates a pre-parsed JsonPath AST against a JSON document. + /// @param ast the parsed JsonPath AST + /// @param json the JSON document to query + /// @return a list of matching JsonValue instances (maybe empty) + static List evaluate(JsonPathAst.Root ast, JsonValue json) { + Objects.requireNonNull(ast, "ast must not be null"); + Objects.requireNonNull(json, "json must not be null"); + + final var results = new ArrayList(); + evaluateSegments(ast.segments(), 0, json, json, results); + return results; + } + + static void evaluateSegments( + List segments, + int index, + JsonValue current, + JsonValue root, + List results) { + + // If we've processed all segments, current is a match + if (index >= segments.size()) { + results.add(current); + return; + } + + final var segment = segments.get(index); + LOG.finer(() -> "Evaluating segment " + index + ": " + segment); + + switch (segment) { + case JsonPathAst.PropertyAccess prop -> evaluatePropertyAccess(prop, segments, index, current, root, results); + case JsonPathAst.ArrayIndex arr -> evaluateArrayIndex(arr, segments, index, current, root, results); + case JsonPathAst.ArraySlice slice -> evaluateArraySlice(slice, segments, index, current, root, results); + case JsonPathAst.Wildcard ignored -> evaluateWildcard(segments, index, current, root, results); + case JsonPathAst.RecursiveDescent desc -> evaluateRecursiveDescent(desc, segments, index, current, root, results); + case JsonPathAst.Filter filter -> evaluateFilter(filter, segments, index, current, root, results); + case JsonPathAst.Union union -> evaluateUnion(union, segments, index, current, root, results); + case JsonPathAst.ScriptExpression script -> evaluateScriptExpression(script, segments, index, current, root, results); + } + } + + static void evaluatePropertyAccess( + JsonPathAst.PropertyAccess prop, + List segments, + int index, + JsonValue current, + JsonValue root, + List results) { + + if (current instanceof JsonObject obj) { + final var value = obj.members().get(prop.name()); + if (value != null) { + evaluateSegments(segments, index + 1, value, root, results); + } + } + } + + static void evaluateArrayIndex( + JsonPathAst.ArrayIndex arr, + List segments, + int index, + JsonValue current, + JsonValue root, + List results) { + + if (current instanceof JsonArray array) { + final var elements = array.elements(); + int idx = arr.index(); + + // Handle negative indices (from end) + if (idx < 0) { + idx = elements.size() + idx; + } + + if (idx >= 0 && idx < elements.size()) { + evaluateSegments(segments, index + 1, elements.get(idx), root, results); + } + } + } + + static void evaluateArraySlice( + JsonPathAst.ArraySlice slice, + List segments, + int index, + JsonValue current, + JsonValue root, + List results) { + + if (current instanceof JsonArray array) { + final var elements = array.elements(); + final int size = elements.size(); + + final int step = slice.step() != null ? slice.step() : 1; + + if (step == 0) { + return; // Invalid step + } + + if (step > 0) { + int start = slice.start() != null ? normalizeIndex(slice.start(), size) : 0; + int end = slice.end() != null ? normalizeIndex(slice.end(), size) : size; + + start = Math.max(0, Math.min(start, size)); + end = Math.max(0, Math.min(end, size)); + + for (int i = start; i < end; i += step) { + evaluateSegments(segments, index + 1, elements.get(i), root, results); + } + } else { + int start = slice.start() != null ? normalizeIndex(slice.start(), size) : size - 1; + final int end = slice.end() != null ? normalizeIndex(slice.end(), size) : -1; + + start = Math.max(0, Math.min(start, size - 1)); + + for (int i = start; i > end; i += step) { + evaluateSegments(segments, index + 1, elements.get(i), root, results); + } + } + } + } + + static int normalizeIndex(int index, int size) { + if (index < 0) { + return size + index; + } + return index; + } + + static void evaluateWildcard( + List segments, + int index, + JsonValue current, + JsonValue root, + List results) { + + if (current instanceof JsonObject obj) { + for (final var value : obj.members().values()) { + evaluateSegments(segments, index + 1, value, root, results); + } + } else if (current instanceof JsonArray array) { + for (final var element : array.elements()) { + evaluateSegments(segments, index + 1, element, root, results); + } + } + } + + static void evaluateRecursiveDescent( + JsonPathAst.RecursiveDescent desc, + List segments, + int index, + JsonValue current, + JsonValue root, + List results) { + + // First, try matching the target at current level + evaluateTargetSegment(desc.target(), segments, index, current, root, results); + + // Then recurse into children + if (current instanceof JsonObject obj) { + for (final var value : obj.members().values()) { + evaluateRecursiveDescent(desc, segments, index, value, root, results); + } + } else if (current instanceof JsonArray array) { + for (final var element : array.elements()) { + evaluateRecursiveDescent(desc, segments, index, element, root, results); + } + } + } + + static void evaluateTargetSegment( + JsonPathAst.Segment target, + List segments, + int index, + JsonValue current, + JsonValue root, + List results) { + + switch (target) { + case JsonPathAst.PropertyAccess prop -> { + if (current instanceof JsonObject obj) { + final var value = obj.members().get(prop.name()); + if (value != null) { + evaluateSegments(segments, index + 1, value, root, results); + } + } + } + case JsonPathAst.Wildcard ignored -> { + if (current instanceof JsonObject obj) { + for (final var value : obj.members().values()) { + evaluateSegments(segments, index + 1, value, root, results); + } + } else if (current instanceof JsonArray array) { + for (final var element : array.elements()) { + evaluateSegments(segments, index + 1, element, root, results); + } + } + } + case JsonPathAst.ArrayIndex arr -> { + if (current instanceof JsonArray array) { + final var elements = array.elements(); + int idx = arr.index(); + if (idx < 0) idx = elements.size() + idx; + if (idx >= 0 && idx < elements.size()) { + evaluateSegments(segments, index + 1, elements.get(idx), root, results); + } + } + } + default -> // Other segment types in recursive descent context + LOG.finer(() -> "Unsupported target in recursive descent: " + target); + } + } + + static void evaluateFilter( + JsonPathAst.Filter filter, + List segments, + int index, + JsonValue current, + JsonValue root, + List results) { + + if (current instanceof JsonArray array) { + for (final var element : array.elements()) { + if (matchesFilter(filter.expression(), element)) { + evaluateSegments(segments, index + 1, element, root, results); + } + } + } + } + + static boolean matchesFilter(JsonPathAst.FilterExpression expr, JsonValue current) { + return switch (expr) { + case JsonPathAst.ExistsFilter exists -> { + final var value = resolvePropertyPath(exists.path(), current); + yield value != null; + } + case JsonPathAst.ComparisonFilter comp -> { + final var leftValue = resolveFilterExpression(comp.left(), current); + final var rightValue = resolveFilterExpression(comp.right(), current); + yield compareValues(leftValue, comp.op(), rightValue); + } + case JsonPathAst.LogicalFilter logical -> { + final var leftMatch = matchesFilter(logical.left(), current); + yield switch (logical.op()) { + case AND -> leftMatch && matchesFilter(logical.right(), current); + case OR -> leftMatch || matchesFilter(logical.right(), current); + case NOT -> !leftMatch; + }; + } + case JsonPathAst.CurrentNode ignored1 -> true; + case JsonPathAst.PropertyPath path -> resolvePropertyPath(path, current) != null; + case JsonPathAst.LiteralValue ignored -> true; + }; + } + + static Object resolveFilterExpression(JsonPathAst.FilterExpression expr, JsonValue current) { + return switch (expr) { + case JsonPathAst.PropertyPath path -> { + final var value = resolvePropertyPath(path, current); + yield jsonValueToComparable(value); + } + case JsonPathAst.LiteralValue lit -> lit.value(); + case JsonPathAst.CurrentNode ignored -> jsonValueToComparable(current); + default -> null; + }; + } + + static JsonValue resolvePropertyPath(JsonPathAst.PropertyPath path, JsonValue current) { + JsonValue value = current; + for (final var prop : path.properties()) { + if (value instanceof JsonObject obj) { + value = obj.members().get(prop); + if (value == null) { + return null; + } + } else { + return null; + } + } + return value; + } + + static Object jsonValueToComparable(JsonValue value) { + if (value == null) return null; + return switch (value) { + case JsonString s -> s.string(); + case JsonNumber n -> n.toDouble(); + case JsonBoolean b -> b.bool(); + case JsonNull ignored -> null; + default -> value; + }; + } + + @SuppressWarnings("unchecked") + static boolean compareValues(Object left, JsonPathAst.ComparisonOp op, Object right) { + if (left == null || right == null) { + return switch (op) { + case EQ -> left == right; + case NE -> left != right; + default -> false; + }; + } + + // Try numeric comparison + switch (left) { + case Number leftNum when right instanceof Number rightNum -> { + final double l = leftNum.doubleValue(); + final double r = rightNum.doubleValue(); + return switch (op) { + case EQ -> l == r; + case NE -> l != r; + case LT -> l < r; + case LE -> l <= r; + case GT -> l > r; + case GE -> l >= r; + }; + } + + + // String comparison + case String ignored when right instanceof String -> { + @SuppressWarnings("rawtypes") final int cmp = ((Comparable) left).compareTo(right); + return switch (op) { + case EQ -> cmp == 0; + case NE -> cmp != 0; + case LT -> cmp < 0; + case LE -> cmp <= 0; + case GT -> cmp > 0; + case GE -> cmp >= 0; + }; + } + + + // Boolean comparison + case Boolean ignored when right instanceof Boolean -> { + return switch (op) { + case EQ -> left.equals(right); + case NE -> !left.equals(right); + default -> false; + }; + } + default -> { + } + } + + // Fallback equality + return switch (op) { + case EQ -> left.equals(right); + case NE -> !left.equals(right); + default -> false; + }; + } + + static void evaluateUnion( + JsonPathAst.Union union, + List segments, + int index, + JsonValue current, + JsonValue root, + List results) { + + for (final var selector : union.selectors()) { + switch (selector) { + case JsonPathAst.ArrayIndex arr -> evaluateArrayIndex(arr, segments, index, current, root, results); + case JsonPathAst.PropertyAccess prop -> evaluatePropertyAccess(prop, segments, index, current, root, results); + default -> LOG.finer(() -> "Unsupported selector in union: " + selector); + } + } + } + + static void evaluateScriptExpression( + JsonPathAst.ScriptExpression script, + List segments, + int index, + JsonValue current, + JsonValue root, + List results) { + + if (current instanceof JsonArray array) { + // Simple support for @.length-1 pattern + final var scriptText = script.script().trim(); + if (scriptText.equals("@.length-1")) { + final int lastIndex = array.elements().size() - 1; + if (lastIndex >= 0) { + evaluateSegments(segments, index + 1, array.elements().get(lastIndex), root, results); + } + } else { + LOG.warning(() -> "Unsupported script expression: " + scriptText); + } + } + } + + static String reconstruct(JsonPathAst.Root root) { + final var sb = new StringBuilder("$"); + for (final var segment : root.segments()) { + appendSegment(sb, segment); + } + return sb.toString(); + } + + static void appendSegment(StringBuilder sb, JsonPathAst.Segment segment) { + switch (segment) { + case JsonPathAst.PropertyAccess prop -> { + if (isSimpleIdentifier(prop.name())) { + sb.append(".").append(prop.name()); + } else { + sb.append("['").append(escape(prop.name())).append("']"); + } + } + case JsonPathAst.ArrayIndex arr -> sb.append("[").append(arr.index()).append("]"); + case JsonPathAst.ArraySlice slice -> { + sb.append("["); + if (slice.start() != null) sb.append(slice.start()); + sb.append(":"); + if (slice.end() != null) sb.append(slice.end()); + if (slice.step() != null) sb.append(":").append(slice.step()); + sb.append("]"); + } + case JsonPathAst.Wildcard ignored -> sb.append(".*"); + case JsonPathAst.RecursiveDescent desc -> { + sb.append(".."); + // RecursiveDescent target is usually PropertyAccess or Wildcard, + // but can be other things in theory. + // If target is PropertyAccess("foo"), append "foo". + // If target is Wildcard, append "*". + // Our AST structure wraps the target segment. + // We need to handle how it's appended. + // appendSegment prepends "." or "[" usually. + // But ".." replaces the dot. + // Let special case the target printing. + appendRecursiveTarget(sb, desc.target()); + } + case JsonPathAst.Filter filter -> { + sb.append("[?("); + appendFilterExpression(sb, filter.expression()); + sb.append(")]"); + } + case JsonPathAst.Union union -> { + sb.append("["); + final var selectors = union.selectors(); + for (int i = 0; i < selectors.size(); i++) { + if (i > 0) sb.append(","); + appendUnionSelector(sb, selectors.get(i)); + } + sb.append("]"); + } + case JsonPathAst.ScriptExpression script -> sb.append("[(").append(script.script()).append(")]"); + } + } + + static void appendRecursiveTarget(StringBuilder sb, JsonPathAst.Segment target) { + if (target instanceof JsonPathAst.PropertyAccess(String name)) { + sb.append(name); // ..name + } else if (target instanceof JsonPathAst.Wildcard) { + sb.append("*"); // ..* + } else { + // Fallback for other types if they ever occur in recursive position + appendSegment(sb, target); + } + } + + static void appendUnionSelector(StringBuilder sb, JsonPathAst.Segment selector) { + if (selector instanceof JsonPathAst.PropertyAccess(String name)) { + sb.append("'").append(escape(name)).append("'"); + } else if (selector instanceof JsonPathAst.ArrayIndex(int index)) { + sb.append(index); + } else { + // Fallback + appendSegment(sb, selector); + } + } + + static void appendFilterExpression(StringBuilder sb, JsonPathAst.FilterExpression expr) { + switch (expr) { + case JsonPathAst.ExistsFilter exists -> appendFilterExpression(sb, exists.path()); // Should print the path + case JsonPathAst.ComparisonFilter comp -> { + appendFilterExpression(sb, comp.left()); + sb.append(comp.op().symbol()); + appendFilterExpression(sb, comp.right()); + } + case JsonPathAst.LogicalFilter logical -> { + if (logical.op() == JsonPathAst.LogicalOp.NOT) { + sb.append("!"); + appendFilterExpression(sb, logical.left()); + } else { + sb.append("("); + appendFilterExpression(sb, logical.left()); + sb.append(" ").append(logical.op().symbol()).append(" "); + appendFilterExpression(sb, logical.right()); + sb.append(")"); + } + } + case JsonPathAst.CurrentNode ignored -> sb.append("@"); + case JsonPathAst.PropertyPath path -> { + sb.append("@"); + for (String p : path.properties()) { + if (isSimpleIdentifier(p)) { + sb.append(".").append(p); + } else { + sb.append("['").append(escape(p)).append("']"); + } + } + } + case JsonPathAst.LiteralValue lit -> { + if (lit.value() instanceof String s) { + sb.append("'").append(escape(s)).append("'"); + } else { + sb.append(lit.value()); + } + } + } + } + + static boolean isSimpleIdentifier(String name) { + if (name.isEmpty()) return false; + if (!Character.isJavaIdentifierStart(name.charAt(0))) return false; + for (int i = 1; i < name.length(); i++) { + if (!Character.isJavaIdentifierPart(name.charAt(i))) return false; + } + return true; + } + + static String escape(String s) { + return s.replace("'", "\\'"); + } +} From 995d626dffd0a9b3bbebfd2e2f87d7fdb8cf8c96 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:30:25 +0000 Subject: [PATCH 2/5] Issue #128 Implement runtime compilation for JsonPath queries Added JDK compiler-based runtime compilation for JsonPath: - JsonPathExecutor: Public functional interface for compiled executors - JsonPathHelpers: Public utility class with helper methods for generated code (getPath, toComparable, compareValues, normalizeIdx, evaluateRecursiveDescent) - JsonPathCompiler: Generates and compiles Java source code at runtime using javax.tools.ToolProvider for in-memory compilation - JsonPathCompiled: Executes compiled JsonPath queries Code generation covers all JsonPath features: - Property access (dot and bracket notation) - Array indexing (positive and negative) - Array slicing (start:end:step, reverse) - Wildcards (* on objects and arrays) - Filters (exists, comparison, logical AND/OR/NOT) - Unions (multiple indices or properties) - Recursive descent (.. for all descendants) - Script expressions (@.length-1 pattern) Added comprehensive test suite (JsonPathCompilerTest) with 40 tests verifying compiled paths produce identical results to interpreted paths. All 140 tests pass. To compile a JsonPath for optimal performance: JsonPath compiled = JsonPath.compile(JsonPath.parse("$.store.book[*].author")); List results = compiled.query(json); Co-authored-by: simbo1905 --- .../java21/jsonpath/JsonPathCompiled.java | 10 - .../java21/jsonpath/JsonPathCompiler.java | 309 +++++--------- .../java21/jsonpath/JsonPathExecutor.java | 16 + .../json/java21/jsonpath/JsonPathHelpers.java | 219 ++++++++++ .../json/java21/jsonpath/JsonPathAstTest.java | 2 +- .../java21/jsonpath/JsonPathCompilerTest.java | 380 ++++++++++++++++++ .../JsonPathFilterEvaluationTest.java | 14 +- .../java21/jsonpath/JsonPathGoessnerTest.java | 4 +- 8 files changed, 721 insertions(+), 233 deletions(-) create mode 100644 json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathExecutor.java create mode 100644 json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathHelpers.java create mode 100644 json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathCompilerTest.java diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java index 825b384..8174fe7 100644 --- a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java @@ -38,14 +38,4 @@ public List query(JsonValue json) { public String toString() { return originalPath; } - - /// Functional interface for compiled JsonPath executors. - @FunctionalInterface - interface JsonPathExecutor { - /// Executes the compiled JsonPath query against a JSON document. - /// @param current the current node being evaluated - /// @param root the root document (for $ references in filters) - /// @return a list of matching JsonValue instances - List execute(JsonValue current, JsonValue root); - } } diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java index 7802b7e..76eb979 100644 --- a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java @@ -58,7 +58,8 @@ static String generateSourceCode(String className, JsonPathAst.Root ast) { package json.java21.jsonpath.generated; import jdk.sandbox.java.util.json.*; - import json.java21.jsonpath.JsonPathCompiled.JsonPathExecutor; + import json.java21.jsonpath.JsonPathExecutor; + import json.java21.jsonpath.JsonPathHelpers; import java.util.*; public final class %s implements JsonPathExecutor { @@ -71,90 +72,10 @@ public List execute(JsonValue current, JsonValue root) { // Generate the evaluation code generateSegmentEvaluation(sb, ast.segments(), 0, "current", "root", "results"); - // Close the execute method and add helper methods + // Close the execute method and class sb.append(""" return results; } - - // Helper methods for generated code - - private static int normalizeIdx(int index, int size) { - return index < 0 ? size + index : index; - } - - private static JsonValue getPath(JsonValue current, String... props) { - JsonValue value = current; - for (String prop : props) { - if (value instanceof JsonObject obj) { - value = obj.members().get(prop); - if (value == null) return null; - } else { - return null; - } - } - return value; - } - - private static Object toComparable(JsonValue value) { - if (value == null) return null; - if (value instanceof JsonString s) return s.string(); - if (value instanceof JsonNumber n) return n.toDouble(); - if (value instanceof JsonBoolean b) return b.bool(); - if (value instanceof JsonNull) return null; - return value; - } - - private static boolean compareValues(Object left, String op, Object right) { - if (left == null || right == null) { - return switch (op) { - case "EQ" -> left == right; - case "NE" -> left != right; - default -> false; - }; - } - - if (left instanceof Number ln && right instanceof Number rn) { - double l = ln.doubleValue(); - double r = rn.doubleValue(); - return switch (op) { - case "EQ" -> l == r; - case "NE" -> l != r; - case "LT" -> l < r; - case "LE" -> l <= r; - case "GT" -> l > r; - case "GE" -> l >= r; - default -> false; - }; - } - - if (left instanceof String && right instanceof String) { - @SuppressWarnings("unchecked") - int cmp = ((Comparable) left).compareTo(right); - return switch (op) { - case "EQ" -> cmp == 0; - case "NE" -> cmp != 0; - case "LT" -> cmp < 0; - case "LE" -> cmp <= 0; - case "GT" -> cmp > 0; - case "GE" -> cmp >= 0; - default -> false; - }; - } - - if (left instanceof Boolean && right instanceof Boolean) { - return switch (op) { - case "EQ" -> left.equals(right); - case "NE" -> !left.equals(right); - default -> false; - }; - } - - return switch (op) { - case "EQ" -> left.equals(right); - case "NE" -> !left.equals(right); - default -> false; - }; - } } """); @@ -265,9 +186,9 @@ private static void generateArraySlice( if (step > 0) { final var startExpr = slice.start() != null ? - "normalizeIdx(%d, size%d)".formatted(slice.start(), nextIndex) : "0"; + "JsonPathHelpers.normalizeIdx(%d, size%d)".formatted(slice.start(), nextIndex) : "0"; final var endExpr = slice.end() != null ? - "normalizeIdx(%d, size%d)".formatted(slice.end(), nextIndex) : "size" + nextIndex; + "JsonPathHelpers.normalizeIdx(%d, size%d)".formatted(slice.end(), nextIndex) : "size" + nextIndex; sb.append(" int start%d = %s;\n".formatted(nextIndex, startExpr)); sb.append(" int end%d = %s;\n".formatted(nextIndex, endExpr)); @@ -277,9 +198,9 @@ private static void generateArraySlice( .formatted(nextIndex, nextIndex, nextIndex, nextIndex, nextIndex, nextIndex)); } else { final var startExpr = slice.start() != null ? - "normalizeIdx(%d, size%d)".formatted(slice.start(), nextIndex) : "size%d - 1".formatted(nextIndex); + "JsonPathHelpers.normalizeIdx(%d, size%d)".formatted(slice.start(), nextIndex) : "size%d - 1".formatted(nextIndex); final var endExpr = slice.end() != null ? - "normalizeIdx(%d, size%d)".formatted(slice.end(), nextIndex) : "-1"; + "JsonPathHelpers.normalizeIdx(%d, size%d)".formatted(slice.end(), nextIndex) : "-1"; sb.append(" int start%d = %s;\n".formatted(nextIndex, startExpr)); sb.append(" int end%d = %s;\n".formatted(nextIndex, endExpr)); @@ -324,22 +245,87 @@ private static void generateRecursiveDescent( String rootVar, String resultsVar) { - // For recursive descent, we generate a helper method call - // For now, fall back to interpreter for complex recursive descent - sb.append(" // Recursive descent - delegating to interpreter\n"); - sb.append(" json.java21.jsonpath.JsonPathInterpreted.evaluateRecursiveDescent(\n"); - sb.append(" new json.java21.jsonpath.JsonPathAst.RecursiveDescent("); - generateAstLiteral(sb, desc.target()); - sb.append("),\n"); - sb.append(" java.util.List.of("); - for (int i = segmentIndex; i < segments.size(); i++) { - if (i > segmentIndex) sb.append(", "); - generateAstLiteral(sb, segments.get(i)); + final int nextIndex = segmentIndex + 1; + + // For simple cases (property access or wildcard with no following segments), generate inline + if (nextIndex >= segments.size()) { + // Terminal recursive descent - generate simple code + switch (desc.target()) { + case JsonPathAst.PropertyAccess prop -> { + sb.append(" // Recursive descent for property: %s\n".formatted(prop.name())); + sb.append(" JsonPathHelpers.evaluateRecursiveDescent(\"%s\", %s, %s);\n" + .formatted(escapeJavaString(prop.name()), currentVar, resultsVar)); + } + case JsonPathAst.Wildcard ignored -> { + sb.append(" // Recursive descent wildcard\n"); + sb.append(" JsonPathHelpers.evaluateRecursiveDescent(null, %s, %s);\n" + .formatted(currentVar, resultsVar)); + } + default -> { + // Complex target - generate a recursive helper inline + sb.append(" // Recursive descent with complex target - inline generation\n"); + generateRecursiveDescentInline(sb, desc, segments, segmentIndex, currentVar, rootVar, resultsVar); + } + } + } else { + // Has following segments - need to generate more complex code + generateRecursiveDescentInline(sb, desc, segments, segmentIndex, currentVar, rootVar, resultsVar); + } + } + + private static void generateRecursiveDescentInline( + StringBuilder sb, + JsonPathAst.RecursiveDescent desc, + List segments, + int segmentIndex, + String currentVar, + String rootVar, + String resultsVar) { + + final int nextIndex = segmentIndex + 1; + final var collectVar = "rdCollect" + nextIndex; + final var iterVar = "rdItem" + nextIndex; + + // For complex recursive descent, we collect intermediate results first + // then process them with subsequent segments + switch (desc.target()) { + case JsonPathAst.PropertyAccess prop -> { + sb.append(" // Recursive descent for property with following segments: %s\n".formatted(prop.name())); + sb.append(" final var %s = new ArrayList();\n".formatted(collectVar)); + sb.append(" JsonPathHelpers.evaluateRecursiveDescent(\"%s\", %s, %s);\n" + .formatted(escapeJavaString(prop.name()), currentVar, collectVar)); + sb.append(" for (final var %s : %s) {\n".formatted(iterVar, collectVar)); + generateSegmentEvaluation(sb, segments, nextIndex, iterVar, rootVar, resultsVar); + sb.append(" }\n"); + } + case JsonPathAst.Wildcard ignored -> { + sb.append(" // Recursive descent wildcard with following segments\n"); + sb.append(" final var %s = new ArrayList();\n".formatted(collectVar)); + sb.append(" JsonPathHelpers.evaluateRecursiveDescent(null, %s, %s);\n" + .formatted(currentVar, collectVar)); + sb.append(" for (final var %s : %s) {\n".formatted(iterVar, collectVar)); + generateSegmentEvaluation(sb, segments, nextIndex, iterVar, rootVar, resultsVar); + sb.append(" }\n"); + } + case JsonPathAst.ArrayIndex arr -> { + // Recursive descent with array index target (e.g., $..book[2]) + sb.append(" // Recursive descent for array index: [%d]\n".formatted(arr.index())); + sb.append(" final var %s = new ArrayList();\n".formatted(collectVar)); + // First collect all arrays + sb.append(" JsonPathHelpers.collectArrays(%s, %s);\n".formatted(currentVar, collectVar)); + sb.append(" for (final var arrItem%d : %s) {\n".formatted(nextIndex, collectVar)); + generateArrayIndex(sb, arr, segments, nextIndex, "arrItem" + nextIndex, rootVar, resultsVar); + sb.append(" }\n"); + } + default -> { + // Unsupported target - add results of current + sb.append(" // Unsupported recursive descent target - adding current\n"); + sb.append(" %s.add(%s);\n".formatted(resultsVar, currentVar)); + } } - sb.append("),\n"); - sb.append(" %d, %s, %s, %s);\n".formatted(segmentIndex, currentVar, rootVar, resultsVar)); } + private static void generateFilter( StringBuilder sb, JsonPathAst.Filter filter, @@ -368,7 +354,7 @@ private static void generateFilterCondition(StringBuilder sb, JsonPathAst.Filter sb.append(" != null"); } case JsonPathAst.ComparisonFilter comp -> { - sb.append("compareValues("); + sb.append("JsonPathHelpers.compareValues("); generateFilterValue(sb, comp.left(), currentVar); sb.append(", \"").append(comp.op().name()).append("\", "); generateFilterValue(sb, comp.right(), currentVar); @@ -407,7 +393,7 @@ private static void generateFilterCondition(StringBuilder sb, JsonPathAst.Filter } private static void generatePropertyPathAccess(StringBuilder sb, JsonPathAst.PropertyPath path, String currentVar) { - sb.append("getPath(%s".formatted(currentVar)); + sb.append("JsonPathHelpers.getPath(%s".formatted(currentVar)); for (final var prop : path.properties()) { sb.append(", \"").append(escapeJavaString(prop)).append("\""); } @@ -417,7 +403,7 @@ private static void generatePropertyPathAccess(StringBuilder sb, JsonPathAst.Pro private static void generateFilterValue(StringBuilder sb, JsonPathAst.FilterExpression expr, String currentVar) { switch (expr) { case JsonPathAst.PropertyPath path -> { - sb.append("toComparable("); + sb.append("JsonPathHelpers.toComparable("); generatePropertyPathAccess(sb, path, currentVar); sb.append(")"); } @@ -435,7 +421,7 @@ private static void generateFilterValue(StringBuilder sb, JsonPathAst.FilterExpr } } case JsonPathAst.CurrentNode ignored -> - sb.append("toComparable(%s)".formatted(currentVar)); + sb.append("JsonPathHelpers.toComparable(%s)".formatted(currentVar)); default -> sb.append("null"); } } @@ -482,119 +468,16 @@ private static void generateScriptExpression( sb.append(" }\n"); sb.append(" }\n"); } else { - // Unsupported script, delegate to interpreter - sb.append(" // Unsupported script expression - delegating to interpreter\n"); - sb.append(" json.java21.jsonpath.JsonPathInterpreted.evaluateScriptExpression(\n"); - sb.append(" new json.java21.jsonpath.JsonPathAst.ScriptExpression(\""); - sb.append(escapeJavaString(script.script())); - sb.append("\"),\n"); - sb.append(" java.util.List.of("); - for (int i = nextIndex - 1; i < segments.size(); i++) { - if (i > nextIndex - 1) sb.append(", "); - generateAstLiteral(sb, segments.get(i)); - } - sb.append("),\n"); - sb.append(" %d, %s, %s, %s);\n".formatted(nextIndex - 1, currentVar, rootVar, resultsVar)); - } - } - - private static void generateAstLiteral(StringBuilder sb, JsonPathAst.Segment segment) { - switch (segment) { - case JsonPathAst.PropertyAccess prop -> - sb.append("new json.java21.jsonpath.JsonPathAst.PropertyAccess(\"") - .append(escapeJavaString(prop.name())).append("\")"); - case JsonPathAst.ArrayIndex arr -> - sb.append("new json.java21.jsonpath.JsonPathAst.ArrayIndex(").append(arr.index()).append(")"); - case JsonPathAst.Wildcard ignored -> - sb.append("new json.java21.jsonpath.JsonPathAst.Wildcard()"); - case JsonPathAst.ArraySlice slice -> { - sb.append("new json.java21.jsonpath.JsonPathAst.ArraySlice("); - sb.append(slice.start() == null ? "null" : slice.start()).append(", "); - sb.append(slice.end() == null ? "null" : slice.end()).append(", "); - sb.append(slice.step() == null ? "null" : slice.step()).append(")"); - } - case JsonPathAst.RecursiveDescent desc -> { - sb.append("new json.java21.jsonpath.JsonPathAst.RecursiveDescent("); - generateAstLiteral(sb, desc.target()); - sb.append(")"); - } - case JsonPathAst.Filter filter -> { - sb.append("new json.java21.jsonpath.JsonPathAst.Filter("); - generateFilterExprLiteral(sb, filter.expression()); - sb.append(")"); - } - case JsonPathAst.Union union -> { - sb.append("new json.java21.jsonpath.JsonPathAst.Union(java.util.List.of("); - final var selectors = union.selectors(); - for (int i = 0; i < selectors.size(); i++) { - if (i > 0) sb.append(", "); - generateAstLiteral(sb, selectors.get(i)); - } - sb.append("))"); - } - case JsonPathAst.ScriptExpression script -> - sb.append("new json.java21.jsonpath.JsonPathAst.ScriptExpression(\"") - .append(escapeJavaString(script.script())).append("\")"); - } - } - - private static void generateFilterExprLiteral(StringBuilder sb, JsonPathAst.FilterExpression expr) { - switch (expr) { - case JsonPathAst.ExistsFilter exists -> { - sb.append("new json.java21.jsonpath.JsonPathAst.ExistsFilter("); - generateFilterExprLiteral(sb, exists.path()); - sb.append(")"); - } - case JsonPathAst.ComparisonFilter comp -> { - sb.append("new json.java21.jsonpath.JsonPathAst.ComparisonFilter("); - generateFilterExprLiteral(sb, comp.left()); - sb.append(", json.java21.jsonpath.JsonPathAst.ComparisonOp.").append(comp.op().name()).append(", "); - generateFilterExprLiteral(sb, comp.right()); - sb.append(")"); - } - case JsonPathAst.LogicalFilter logical -> { - sb.append("new json.java21.jsonpath.JsonPathAst.LogicalFilter("); - generateFilterExprLiteral(sb, logical.left()); - sb.append(", json.java21.jsonpath.JsonPathAst.LogicalOp.").append(logical.op().name()).append(", "); - if (logical.right() != null) { - generateFilterExprLiteral(sb, logical.right()); - } else { - sb.append("null"); - } - sb.append(")"); - } - case JsonPathAst.CurrentNode ignored -> - sb.append("new json.java21.jsonpath.JsonPathAst.CurrentNode()"); - case JsonPathAst.PropertyPath path -> { - sb.append("new json.java21.jsonpath.JsonPathAst.PropertyPath(java.util.List.of("); - for (int i = 0; i < path.properties().size(); i++) { - if (i > 0) sb.append(", "); - sb.append("\"").append(escapeJavaString(path.properties().get(i))).append("\""); - } - sb.append("))"); - } - case JsonPathAst.LiteralValue lit -> { - sb.append("new json.java21.jsonpath.JsonPathAst.LiteralValue("); - if (lit.value() == null) { - sb.append("null"); - } else if (lit.value() instanceof String s) { - sb.append("\"").append(escapeJavaString(s)).append("\""); - } else if (lit.value() instanceof Number n) { - if (lit.value() instanceof Long l) { - sb.append(l).append("L"); - } else { - sb.append(n.doubleValue()); - } - } else if (lit.value() instanceof Boolean b) { - sb.append(b); - } - sb.append(")"); - } + // Unsupported script - log warning and skip + // Only @.length-1 is supported in compiled mode + sb.append(" // WARNING: Unsupported script expression '%s' - skipped in compiled mode\n" + .formatted(escapeJavaString(script.script()))); + sb.append(" // Consider using slice notation instead: [-1:] for last element\n"); } } /// Compiles the generated source code and instantiates the executor. - private static JsonPathCompiled.JsonPathExecutor compileAndInstantiate(String className, String sourceCode) { + private static JsonPathExecutor compileAndInstantiate(String className, String sourceCode) { final var compiler = ToolProvider.getSystemJavaCompiler(); if (compiler == null) { LOG.warning(() -> "No Java compiler available - falling back to interpreter delegation"); @@ -633,7 +516,7 @@ private static JsonPathCompiled.JsonPathExecutor compileAndInstantiate(String cl try { final var classLoader = new InMemoryClassLoader(fileManager.getClassBytes()); final var clazz = classLoader.loadClass(fullClassName); - return (JsonPathCompiled.JsonPathExecutor) clazz.getDeclaredConstructor().newInstance(); + return (JsonPathExecutor) clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException("Failed to instantiate compiled JsonPath executor", e); } diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathExecutor.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathExecutor.java new file mode 100644 index 0000000..9194901 --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathExecutor.java @@ -0,0 +1,16 @@ +package json.java21.jsonpath; + +import jdk.sandbox.java.util.json.JsonValue; + +import java.util.List; + +/// Functional interface for compiled JsonPath executors. +/// This interface is public to allow generated code to implement it. +@FunctionalInterface +public interface JsonPathExecutor { + /// Executes the compiled JsonPath query against a JSON document. + /// @param current the current node being evaluated + /// @param root the root document (for $ references in filters) + /// @return a list of matching JsonValue instances + List execute(JsonValue current, JsonValue root); +} diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathHelpers.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathHelpers.java new file mode 100644 index 0000000..0c98fc2 --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathHelpers.java @@ -0,0 +1,219 @@ +package json.java21.jsonpath; + +import jdk.sandbox.java.util.json.*; + +import java.util.List; + +/// Public helper methods for runtime-compiled JsonPath executors. +/// This class provides access to internal evaluation methods that generated code needs. +public final class JsonPathHelpers { + + private JsonPathHelpers() { + // utility class + } + + /// Resolves a property path from a JsonValue. + /// @param current the current JSON value + /// @param props the property path to resolve + /// @return the resolved value, or null if not found + public static JsonValue getPath(JsonValue current, String... props) { + JsonValue value = current; + for (final var prop : props) { + if (value instanceof JsonObject obj) { + value = obj.members().get(prop); + if (value == null) return null; + } else { + return null; + } + } + return value; + } + + /// Converts a JsonValue to a comparable object for filter comparisons. + /// @param value the JSON value to convert + /// @return a comparable object (String, Number, Boolean, or null) + public static Object toComparable(JsonValue value) { + if (value == null) return null; + return switch (value) { + case JsonString s -> s.string(); + case JsonNumber n -> n.toDouble(); + case JsonBoolean b -> b.bool(); + case JsonNull ignored -> null; + default -> value; + }; + } + + /// Compares two values using the specified operator. + /// @param left the left operand + /// @param op the comparison operator name (EQ, NE, LT, LE, GT, GE) + /// @param right the right operand + /// @return true if the comparison is satisfied + @SuppressWarnings("unchecked") + public static boolean compareValues(Object left, String op, Object right) { + if (left == null || right == null) { + return switch (op) { + case "EQ" -> left == right; + case "NE" -> left != right; + default -> false; + }; + } + + // Numeric comparison + if (left instanceof Number leftNum && right instanceof Number rightNum) { + final double l = leftNum.doubleValue(); + final double r = rightNum.doubleValue(); + return switch (op) { + case "EQ" -> l == r; + case "NE" -> l != r; + case "LT" -> l < r; + case "LE" -> l <= r; + case "GT" -> l > r; + case "GE" -> l >= r; + default -> false; + }; + } + + // String comparison + if (left instanceof String && right instanceof String) { + @SuppressWarnings("rawtypes") + final int cmp = ((Comparable) left).compareTo(right); + return switch (op) { + case "EQ" -> cmp == 0; + case "NE" -> cmp != 0; + case "LT" -> cmp < 0; + case "LE" -> cmp <= 0; + case "GT" -> cmp > 0; + case "GE" -> cmp >= 0; + default -> false; + }; + } + + // Boolean comparison + if (left instanceof Boolean && right instanceof Boolean) { + return switch (op) { + case "EQ" -> left.equals(right); + case "NE" -> !left.equals(right); + default -> false; + }; + } + + // Fallback equality + return switch (op) { + case "EQ" -> left.equals(right); + case "NE" -> !left.equals(right); + default -> false; + }; + } + + /// Normalizes an array index (handles negative indices). + /// @param index the index (possibly negative) + /// @param size the array size + /// @return the normalized index + public static int normalizeIdx(int index, int size) { + return index < 0 ? size + index : index; + } + + /// Evaluates recursive descent from a starting value. + /// This is used for $.. patterns that need to search all descendants. + /// @param propertyName the property name to search for (null for wildcard) + /// @param current the current value to search from + /// @param results the list to add matching values to + public static void evaluateRecursiveDescent(String propertyName, JsonValue current, List results) { + // First, try matching at current level + if (propertyName == null) { + // Wildcard - match all children + if (current instanceof JsonObject obj) { + results.addAll(obj.members().values()); + for (final var value : obj.members().values()) { + evaluateRecursiveDescent(null, value, results); + } + } else if (current instanceof JsonArray array) { + results.addAll(array.elements()); + for (final var element : array.elements()) { + evaluateRecursiveDescent(null, element, results); + } + } + } else { + // Named property - match specific property + if (current instanceof JsonObject obj) { + final var value = obj.members().get(propertyName); + if (value != null) { + results.add(value); + } + for (final var child : obj.members().values()) { + evaluateRecursiveDescent(propertyName, child, results); + } + } else if (current instanceof JsonArray array) { + for (final var element : array.elements()) { + evaluateRecursiveDescent(propertyName, element, results); + } + } + } + } + + /// Evaluates recursive descent and then applies subsequent segments. + /// This is a more general version that delegates back to the interpreter for complex cases. + /// @param path the original JsonPath being evaluated + /// @param segmentIndex the index of the recursive descent segment + /// @param current the current value + /// @param root the root document + /// @param results the results list + public static void evaluateRecursiveDescentFull( + JsonPath path, + int segmentIndex, + JsonValue current, + JsonValue root, + List results) { + + // For full recursive descent support, we delegate to the interpreter + // This handles the case where there are segments after the recursive descent + if (path instanceof JsonPathInterpreted interpreted) { + final var ast = interpreted.ast(); + JsonPathInterpreted.evaluateSegments(ast.segments(), segmentIndex, current, root, results); + } else if (path.ast() != null) { + JsonPathInterpreted.evaluateSegments(path.ast().segments(), segmentIndex, current, root, results); + } + } + + /// Collects all arrays recursively from a JSON value. + /// Used for recursive descent with array index targets like $..book[2]. + /// @param current the current JSON value to search + /// @param arrays the list to collect arrays into + public static void collectArrays(JsonValue current, List arrays) { + if (current instanceof JsonArray array) { + arrays.add(array); + for (final var element : array.elements()) { + collectArrays(element, arrays); + } + } else if (current instanceof JsonObject obj) { + for (final var value : obj.members().values()) { + if (value instanceof JsonArray) { + collectArrays(value, arrays); + } else if (value instanceof JsonObject) { + collectArrays(value, arrays); + } + } + } + } + + /// Collects values at a specific property path recursively. + /// Used for recursive descent patterns like $..book. + /// @param propertyName the property name to search for + /// @param current the current JSON value to search + /// @param results the list to collect results into + public static void collectAtPath(String propertyName, JsonValue current, List results) { + if (current instanceof JsonObject obj) { + final var value = obj.members().get(propertyName); + if (value != null) { + results.add(value); + } + for (final var child : obj.members().values()) { + collectAtPath(propertyName, child, results); + } + } else if (current instanceof JsonArray array) { + for (final var element : array.elements()) { + collectAtPath(propertyName, element, results); + } + } + } +} diff --git a/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathAstTest.java b/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathAstTest.java index b2a7823..d0d59f1 100644 --- a/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathAstTest.java +++ b/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathAstTest.java @@ -60,7 +60,7 @@ void testArrayIndex() { LOG.info(() -> "TEST: testArrayIndex"); final var index = new JsonPathAst.ArrayIndex(0); assertThat(index.index()).isEqualTo(0); - + final var negIndex = new JsonPathAst.ArrayIndex(-1); assertThat(negIndex.index()).isEqualTo(-1); } diff --git a/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathCompilerTest.java b/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathCompilerTest.java new file mode 100644 index 0000000..432e499 --- /dev/null +++ b/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathCompilerTest.java @@ -0,0 +1,380 @@ +package json.java21.jsonpath; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonValue; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.logging.Logger; + +import static org.assertj.core.api.Assertions.assertThat; + +/// Tests for JsonPath runtime compilation. +/// Verifies that compiled paths produce identical results to interpreted paths. +class JsonPathCompilerTest extends JsonPathLoggingConfig { + + private static final Logger LOG = Logger.getLogger(JsonPathCompilerTest.class.getName()); + + private static final String STORE_JSON = """ + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + } + } + """; + + private static JsonValue storeJson; + + @BeforeAll + static void parseJson() { + storeJson = Json.parse(STORE_JSON); + LOG.info(() -> "Parsed store JSON for compiler tests"); + } + + // Basic compilation tests + + @Test + void testCompileReturnsCompiledInstance() { + LOG.info(() -> "TEST: testCompileReturnsCompiledInstance"); + final var interpreted = JsonPath.parse("$.store"); + assertThat(interpreted.isInterpreted()).isTrue(); + assertThat(interpreted.isCompiled()).isFalse(); + + final var compiled = JsonPath.compile(interpreted); + assertThat(compiled.isCompiled()).isTrue(); + assertThat(compiled.isInterpreted()).isFalse(); + } + + @Test + void testCompileIdempotent() { + LOG.info(() -> "TEST: testCompileIdempotent - compiling a compiled path returns same instance"); + final var interpreted = JsonPath.parse("$.store"); + final var compiled = JsonPath.compile(interpreted); + final var compiledAgain = JsonPath.compile(compiled); + assertThat(compiledAgain).isSameAs(compiled); + } + + // Simple property access tests + + @Test + void testCompiledRootOnly() { + LOG.info(() -> "TEST: testCompiledRootOnly - $ returns the root document"); + assertCompiledMatchesInterpreted("$", storeJson); + } + + @Test + void testCompiledSingleProperty() { + LOG.info(() -> "TEST: testCompiledSingleProperty - $.store"); + assertCompiledMatchesInterpreted("$.store", storeJson); + } + + @Test + void testCompiledNestedProperty() { + LOG.info(() -> "TEST: testCompiledNestedProperty - $.store.bicycle"); + assertCompiledMatchesInterpreted("$.store.bicycle", storeJson); + } + + @Test + void testCompiledDeepProperty() { + LOG.info(() -> "TEST: testCompiledDeepProperty - $.store.bicycle.color"); + assertCompiledMatchesInterpreted("$.store.bicycle.color", storeJson); + } + + @Test + void testCompiledBracketNotation() { + LOG.info(() -> "TEST: testCompiledBracketNotation - $['store']['book']"); + assertCompiledMatchesInterpreted("$['store']['book']", storeJson); + } + + @Test + void testCompiledPropertyNotFound() { + LOG.info(() -> "TEST: testCompiledPropertyNotFound - $.nonexistent"); + assertCompiledMatchesInterpreted("$.nonexistent", storeJson); + } + + // Array index tests + + @Test + void testCompiledArrayIndexFirst() { + LOG.info(() -> "TEST: testCompiledArrayIndexFirst - $.store.book[0]"); + assertCompiledMatchesInterpreted("$.store.book[0]", storeJson); + } + + @Test + void testCompiledArrayIndexLast() { + LOG.info(() -> "TEST: testCompiledArrayIndexLast - $.store.book[-1]"); + assertCompiledMatchesInterpreted("$.store.book[-1]", storeJson); + } + + @Test + void testCompiledArrayIndexOutOfBounds() { + LOG.info(() -> "TEST: testCompiledArrayIndexOutOfBounds - $.store.book[100]"); + assertCompiledMatchesInterpreted("$.store.book[100]", storeJson); + } + + @Test + void testCompiledArrayIndexChained() { + LOG.info(() -> "TEST: testCompiledArrayIndexChained - $.store.book[0].title"); + assertCompiledMatchesInterpreted("$.store.book[0].title", storeJson); + } + + // Array slice tests + + @Test + void testCompiledArraySliceBasic() { + LOG.info(() -> "TEST: testCompiledArraySliceBasic - $.store.book[:2]"); + assertCompiledMatchesInterpreted("$.store.book[:2]", storeJson); + } + + @Test + void testCompiledArraySliceWithStep() { + LOG.info(() -> "TEST: testCompiledArraySliceWithStep - $.store.book[0:4:2]"); + assertCompiledMatchesInterpreted("$.store.book[0:4:2]", storeJson); + } + + @Test + void testCompiledArraySliceReverse() { + LOG.info(() -> "TEST: testCompiledArraySliceReverse - $.store.book[::-1]"); + assertCompiledMatchesInterpreted("$.store.book[::-1]", storeJson); + } + + @Test + void testCompiledArraySliceNegativeStart() { + LOG.info(() -> "TEST: testCompiledArraySliceNegativeStart - $.store.book[-1:]"); + assertCompiledMatchesInterpreted("$.store.book[-1:]", storeJson); + } + + // Wildcard tests + + @Test + void testCompiledWildcardObject() { + LOG.info(() -> "TEST: testCompiledWildcardObject - $.store.*"); + assertCompiledMatchesInterpreted("$.store.*", storeJson); + } + + @Test + void testCompiledWildcardArray() { + LOG.info(() -> "TEST: testCompiledWildcardArray - $.store.book[*]"); + assertCompiledMatchesInterpreted("$.store.book[*]", storeJson); + } + + @Test + void testCompiledWildcardChained() { + LOG.info(() -> "TEST: testCompiledWildcardChained - $.store.book[*].author"); + assertCompiledMatchesInterpreted("$.store.book[*].author", storeJson); + } + + // Filter tests + + @Test + void testCompiledFilterExists() { + LOG.info(() -> "TEST: testCompiledFilterExists - $.store.book[?(@.isbn)]"); + assertCompiledMatchesInterpreted("$.store.book[?(@.isbn)]", storeJson); + } + + @Test + void testCompiledFilterNotExists() { + LOG.info(() -> "TEST: testCompiledFilterNotExists - $.store.book[?(!@.isbn)]"); + assertCompiledMatchesInterpreted("$.store.book[?(!@.isbn)]", storeJson); + } + + @Test + void testCompiledFilterCompareLessThan() { + LOG.info(() -> "TEST: testCompiledFilterCompareLessThan - $.store.book[?(@.price<10)]"); + assertCompiledMatchesInterpreted("$.store.book[?(@.price<10)]", storeJson); + } + + @Test + void testCompiledFilterCompareGreaterThan() { + LOG.info(() -> "TEST: testCompiledFilterCompareGreaterThan - $.store.book[?(@.price>20)]"); + assertCompiledMatchesInterpreted("$.store.book[?(@.price>20)]", storeJson); + } + + @Test + void testCompiledFilterCompareEquals() { + LOG.info(() -> "TEST: testCompiledFilterCompareEquals - $.store.book[?(@.category=='fiction')]"); + assertCompiledMatchesInterpreted("$.store.book[?(@.category=='fiction')]", storeJson); + } + + @Test + void testCompiledFilterLogicalAnd() { + LOG.info(() -> "TEST: testCompiledFilterLogicalAnd - $.store.book[?(@.isbn && @.price>20)]"); + assertCompiledMatchesInterpreted("$.store.book[?(@.isbn && @.price>20)]", storeJson); + } + + @Test + void testCompiledFilterLogicalOr() { + LOG.info(() -> "TEST: testCompiledFilterLogicalOr - $.store.book[?(@.price<10 || @.price>20)]"); + assertCompiledMatchesInterpreted("$.store.book[?(@.price<10 || @.price>20)]", storeJson); + } + + @Test + void testCompiledFilterComplex() { + LOG.info(() -> "TEST: testCompiledFilterComplex - $.store.book[?(@.isbn && (@.price<10 || @.price>20))]"); + assertCompiledMatchesInterpreted("$.store.book[?(@.isbn && (@.price<10 || @.price>20))]", storeJson); + } + + // Union tests + + @Test + void testCompiledUnionIndices() { + LOG.info(() -> "TEST: testCompiledUnionIndices - $.store.book[0,1]"); + assertCompiledMatchesInterpreted("$.store.book[0,1]", storeJson); + } + + @Test + void testCompiledUnionProperties() { + LOG.info(() -> "TEST: testCompiledUnionProperties - $.store['book','bicycle']"); + assertCompiledMatchesInterpreted("$.store['book','bicycle']", storeJson); + } + + // Recursive descent tests + + @Test + void testCompiledRecursiveDescentProperty() { + LOG.info(() -> "TEST: testCompiledRecursiveDescentProperty - $..author"); + assertCompiledMatchesInterpreted("$..author", storeJson); + } + + @Test + void testCompiledRecursiveDescentPrice() { + LOG.info(() -> "TEST: testCompiledRecursiveDescentPrice - $..price"); + assertCompiledMatchesInterpreted("$..price", storeJson); + } + + @Test + void testCompiledRecursiveDescentWildcard() { + LOG.info(() -> "TEST: testCompiledRecursiveDescentWildcard - $..*"); + assertCompiledMatchesInterpreted("$..*", storeJson); + } + + @Test + void testCompiledRecursiveDescentWithIndex() { + LOG.info(() -> "TEST: testCompiledRecursiveDescentWithIndex - $..book[2]"); + assertCompiledMatchesInterpreted("$..book[2]", storeJson); + } + + // Script expression tests + + @Test + void testCompiledScriptExpressionLastElement() { + LOG.info(() -> "TEST: testCompiledScriptExpressionLastElement - $.store.book[(@.length-1)]"); + assertCompiledMatchesInterpreted("$.store.book[(@.length-1)]", storeJson); + } + + // Code generation verification tests + + @Test + void testCodeGenerationForSimpleProperty() { + LOG.info(() -> "TEST: testCodeGenerationForSimpleProperty - verify generated code compiles"); + final var path = JsonPath.parse("$.store.bicycle.color"); + final var compiled = JsonPath.compile(path); + assertThat(compiled).isInstanceOf(JsonPathCompiled.class); + + // Verify it works + final var results = compiled.query(storeJson); + assertThat(results).hasSize(1); + assertThat(results.getFirst().string()).isEqualTo("red"); + } + + @Test + void testCompiledToStringReconstructsPath() { + LOG.info(() -> "TEST: testCompiledToStringReconstructsPath"); + final var path = JsonPath.parse("$.store.book[*].author"); + final var compiled = JsonPath.compile(path); + + // toString should reconstruct the path + assertThat(compiled.toString()).isEqualTo(path.toString()); + } + + @Test + void testCompiledAstAccessible() { + LOG.info(() -> "TEST: testCompiledAstAccessible"); + final var path = JsonPath.parse("$.store.book"); + final var compiled = JsonPath.compile(path); + + // AST should be accessible from compiled path + assertThat(compiled.ast()).isNotNull(); + assertThat(compiled.ast().segments()).hasSize(2); + } + + // Edge case tests + + @Test + void testCompiledEmptyPath() { + LOG.info(() -> "TEST: testCompiledEmptyPath - $ with no segments"); + final var json = Json.parse("42"); + assertCompiledMatchesInterpreted("$", json); + } + + @Test + void testCompiledOnDifferentDocuments() { + LOG.info(() -> "TEST: testCompiledOnDifferentDocuments - reuse compiled path"); + final var compiled = JsonPath.compile(JsonPath.parse("$.name")); + + final var doc1 = Json.parse("{\"name\": \"Alice\"}"); + final var doc2 = Json.parse("{\"name\": \"Bob\"}"); + + final var results1 = compiled.query(doc1); + final var results2 = compiled.query(doc2); + + assertThat(results1.getFirst().string()).isEqualTo("Alice"); + assertThat(results2.getFirst().string()).isEqualTo("Bob"); + } + + @Test + void testCompiledWithSpecialCharactersInPropertyName() { + LOG.info(() -> "TEST: testCompiledWithSpecialCharactersInPropertyName"); + final var json = Json.parse("{\"special-key\": 123}"); + assertCompiledMatchesInterpreted("$['special-key']", json); + } + + /// Helper method to verify compiled and interpreted paths produce identical results. + private void assertCompiledMatchesInterpreted(String pathExpr, JsonValue json) { + final var interpreted = JsonPath.parse(pathExpr); + final var compiled = JsonPath.compile(interpreted); + + final var interpretedResults = interpreted.query(json); + final var compiledResults = compiled.query(json); + + LOG.fine(() -> "Path: " + pathExpr); + LOG.fine(() -> "Interpreted results: " + interpretedResults.size()); + LOG.fine(() -> "Compiled results: " + compiledResults.size()); + + assertThat(compiledResults) + .as("Compiled results for '%s' should match interpreted results", pathExpr) + .containsExactlyElementsOf(interpretedResults); + } +} diff --git a/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathFilterEvaluationTest.java b/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathFilterEvaluationTest.java index 54db45d..0e3f220 100644 --- a/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathFilterEvaluationTest.java +++ b/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathFilterEvaluationTest.java @@ -80,12 +80,12 @@ void testLogicalNot() { // If @.active exists and is true -> false. // If @.active exists and is false -> true. // If @.active is missing -> ExistsFilter returns false -> !false -> true. - - // However, "ExistsFilter" checks for existence. - // @.active matches id 1 (true) and 2 (false). + + // However, "ExistsFilter" checks for existence. + // @.active matches id 1 (true) and 2 (false). // Wait, ExistsFilter checks if the path *exists* and is non-null. // Let's verify specific behavior for boolean value comparison vs existence. - + // Case A: Existence check negation // [?(!@.active)] -> Match items where "active" does NOT exist. var missingResults = JsonPath.parse("$[?(!@.active)]").query(json); @@ -110,11 +110,11 @@ void testParenthesesPrecedence() { // A=true, B=false, C=true // A && (B || C) -> T && (F || T) -> T && T -> MATCH // (A && B) || C -> (T && F) || T -> F || T -> MATCH (Wait, bad example, both match) - + // Let's try: A=false, B=true, C=true // A && (B || C) -> F && T -> NO MATCH // (A && B) || C -> F || T -> MATCH - + var json = Json.parse(""" [ {"id": 1, "A": false, "B": true, "C": true} @@ -138,7 +138,7 @@ void testParenthesesPrecedence() { void testComplexNestedLogic() { LOG.info(() -> "TEST: testComplexNestedLogic"); // (Price < 10 OR (Category == 'fiction' AND Published is false)) - // Note: !@.published would mean "published does not exist". + // Note: !@.published would mean "published does not exist". // To check for false value, use @.published == false. var json = Json.parse(""" [ diff --git a/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathGoessnerTest.java b/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathGoessnerTest.java index 3fcd3a8..52b0458 100644 --- a/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathGoessnerTest.java +++ b/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathGoessnerTest.java @@ -406,11 +406,11 @@ void testStaticQueryWithCompiledPath() { void testFluentApiReusable() { LOG.info(() -> "TEST: testFluentApiReusable - compiled path can be reused"); final var compiledPath = JsonPath.parse("$..price"); - + // Use on store doc final var storeResults = compiledPath.query(storeJson); assertThat(storeResults).hasSize(5); // 4 book prices + 1 bicycle price - + // Use on a different doc final var simpleDoc = Json.parse(""" {"item": {"price": 99.99}} From cbdd23bfba432135c7df5ca6a417fa54a92db85e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:31:21 +0000 Subject: [PATCH 3/5] Issue #128 Document runtime compilation feature Updated README.md: - Added 'Runtime Compilation (Performance Optimization)' section - Documents how to use JsonPath.compile() for hot paths - Explains when to use compiled vs interpreted paths - Notes compilation is idempotent Updated AGENTS.md: - Documented the sealed interface architecture - Added new entry points for compilation-related classes - Added guidance for maintaining code generation Co-authored-by: simbo1905 --- json-java21-jsonpath/AGENTS.md | 48 ++++++++++++++++++++++++++------ json-java21-jsonpath/README.md | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/json-java21-jsonpath/AGENTS.md b/json-java21-jsonpath/AGENTS.md index b8d9ba7..706d683 100644 --- a/json-java21-jsonpath/AGENTS.md +++ b/json-java21-jsonpath/AGENTS.md @@ -5,13 +5,43 @@ This file is for contributor/agent operational notes. Read `json-java21-jsonpath - User docs MUST recommend only `./mvnw`. - The `$(command -v mvnd || command -v mvn || command -v ./mvnw)` wrapper is for local developer speed only; do not put it in user-facing docs. -Stable code entry points: -- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPath.java` -- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathParser.java` -- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAst.java` -- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathParseException.java` -- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathStreams.java` - -When changing syntax/behavior: -- Update `JsonPathAst` + `JsonPathParser` + `JsonPath` together. +## Architecture + +JsonPath is a sealed interface with two implementations: +- `JsonPathInterpreted`: AST-walking interpreter (default from `JsonPath.parse()`) +- `JsonPathCompiled`: Bytecode-compiled version (from `JsonPath.compile()`) + +The compilation flow: +1. `JsonPath.parse()` -> `JsonPathInterpreted` (fast parsing, AST-based evaluation) +2. `JsonPath.compile()` -> `JsonPathCompiled` (generates Java source, compiles with JDK ToolProvider) + +## Stable Code Entry Points + +- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPath.java` - Public sealed interface +- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathParser.java` - Parses expressions to AST +- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAst.java` - AST node definitions +- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathInterpreted.java` - AST-walking evaluator +- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java` - Code generator and compiler +- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java` - Compiled executor wrapper +- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathExecutor.java` - Public interface for generated executors +- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathHelpers.java` - Helpers for generated code +- `json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathStreams.java` - Stream processing utilities + +## When Changing Syntax/Behavior + +- Update `JsonPathAst` + `JsonPathParser` + `JsonPathInterpreted` together. +- Update `JsonPathCompiler` code generation to match any AST changes. - Add parser + evaluation tests; new tests should extend `JsonPathLoggingConfig`. +- Add compiler tests in `JsonPathCompilerTest` to verify compiled and interpreted produce identical results. + +## Code Generation Notes + +The `JsonPathCompiler` generates Java source code that: +- Imports from `jdk.sandbox.java.util.json.*` and `json.java21.jsonpath.*` +- Implements `JsonPathExecutor` functional interface +- Uses `JsonPathHelpers` for complex operations (recursive descent, comparisons) +- Is compiled in-memory using `javax.tools.ToolProvider` + +When adding new AST node types: +1. Add the case to `generateSegmentEvaluation()` in `JsonPathCompiler` +2. Consider if a helper method in `JsonPathHelpers` would simplify the generated code diff --git a/json-java21-jsonpath/README.md b/json-java21-jsonpath/README.md index f38d36f..938848b 100644 --- a/json-java21-jsonpath/README.md +++ b/json-java21-jsonpath/README.md @@ -60,6 +60,57 @@ This implementation follows Goessner-style JSONPath operators, including: - `[?(@.prop)]` and `[?(@.prop op value)]` basic filters - `[(@.length-1)]` limited script support +## Runtime Compilation (Performance Optimization) + +For performance-critical code paths, you can compile a parsed JsonPath to bytecode: + +```java +// Parse once, compile for best performance +JsonPath compiled = JsonPath.compile(JsonPath.parse("$.store.book[*].author")); + +// Reuse the compiled path for many documents +for (JsonValue doc : documents) { + List results = compiled.query(doc); + // process results... +} +``` + +### How It Works + +The `JsonPath.compile()` method: +1. Takes a parsed (interpreted) JsonPath +2. Generates optimized Java source code +3. Compiles it to bytecode using the JDK compiler API (`javax.tools.ToolProvider`) +4. Returns a compiled JsonPath that executes as native bytecode + +### When to Use Compilation + +- **Hot paths**: Use `compile()` when the same path will be executed many times +- **One-off queries**: Use `parse()` directly for single-use paths (compilation overhead isn't worth it) +- **JRE environments**: Compilation requires a JDK; if unavailable, use interpreted paths + +### Compilation is Idempotent + +Calling `compile()` on an already-compiled path returns the same instance: + +```java +JsonPath interpreted = JsonPath.parse("$.store"); +JsonPath compiled = JsonPath.compile(interpreted); +JsonPath sameCompiled = JsonPath.compile(compiled); // Returns same instance + +// Check if a path is compiled +boolean isCompiled = path.isCompiled(); +boolean isInterpreted = path.isInterpreted(); +``` + +### Supported Features in Compiled Mode + +All JsonPath features are supported in compiled mode: +- Property access, array indices, slices, wildcards +- Recursive descent (`$..property`) +- Filters with comparisons and logical operators +- Unions and script expressions + ## Stream-Based Functions (Aggregations) Some JsonPath implementations include aggregation functions such as `$.numbers.avg()`. From 026b157e6b696611a37acd35013e262e0f35effa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:51:40 +0000 Subject: [PATCH 4/5] Issue #128 Fix unsupported recursive descent targets to match interpreter Addresses review comment: For unsupported recursive descent targets like $..[0:2], $..[?(@.x)], or $..['a','b'], the compiled code was incorrectly adding the current node to results. The interpreter logs and returns no matches for these cases. Changed to generate a no-op comment instead of adding to results, which matches the interpreter's semantics. Co-authored-by: simbo1905 --- .../main/java/json/java21/jsonpath/JsonPathCompiler.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java index 76eb979..02b223a 100644 --- a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java @@ -318,9 +318,10 @@ private static void generateRecursiveDescentInline( sb.append(" }\n"); } default -> { - // Unsupported target - add results of current - sb.append(" // Unsupported recursive descent target - adding current\n"); - sb.append(" %s.add(%s);\n".formatted(resultsVar, currentVar)); + // Unsupported recursive descent target - no-op to match interpreter behavior + // The interpreter logs and returns no matches for unsupported targets like + // $..[0:2], $..[?(@.x)], or $..['a','b'] + sb.append(" // Unsupported recursive descent target (no matches per interpreter semantics)\n"); } } } From 70e92020b09dd5621f5a89aed5acfcc79e4ee817 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:53:14 +0000 Subject: [PATCH 5/5] Issue #128 Address all PR review comments Fixes: 1. Remove unused evaluateRecursiveDescentFull() method from JsonPathHelpers 2. Remove unused collectAtPath() method from JsonPathHelpers 3. Fix README example: use 'compiled'/'interpreted' instead of undefined 'path' 4. Clarify in README that only limited script expressions are supported 5. Throw UnsupportedOperationException for unsupported script expressions instead of silently ignoring them All 140 tests pass. Co-authored-by: simbo1905 --- json-java21-jsonpath/README.md | 6 +-- .../java21/jsonpath/JsonPathCompiler.java | 5 +-- .../json/java21/jsonpath/JsonPathHelpers.java | 44 ------------------- 3 files changed, 5 insertions(+), 50 deletions(-) diff --git a/json-java21-jsonpath/README.md b/json-java21-jsonpath/README.md index 938848b..5646450 100644 --- a/json-java21-jsonpath/README.md +++ b/json-java21-jsonpath/README.md @@ -99,8 +99,8 @@ JsonPath compiled = JsonPath.compile(interpreted); JsonPath sameCompiled = JsonPath.compile(compiled); // Returns same instance // Check if a path is compiled -boolean isCompiled = path.isCompiled(); -boolean isInterpreted = path.isInterpreted(); +boolean isCompiled = compiled.isCompiled(); +boolean isInterpreted = interpreted.isInterpreted(); ``` ### Supported Features in Compiled Mode @@ -109,7 +109,7 @@ All JsonPath features are supported in compiled mode: - Property access, array indices, slices, wildcards - Recursive descent (`$..property`) - Filters with comparisons and logical operators -- Unions and script expressions +- Unions and limited script expressions (e.g., `[(@.length-1)]`) ## Stream-Based Functions (Aggregations) diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java index 02b223a..2bf95d8 100644 --- a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java @@ -469,11 +469,10 @@ private static void generateScriptExpression( sb.append(" }\n"); sb.append(" }\n"); } else { - // Unsupported script - log warning and skip + // Unsupported script - throw exception at runtime for clarity // Only @.length-1 is supported in compiled mode - sb.append(" // WARNING: Unsupported script expression '%s' - skipped in compiled mode\n" + sb.append(" throw new UnsupportedOperationException(\"Unsupported script expression in compiled mode: '%s'. Consider using slice notation instead (e.g., [-1:] for last element).\");\n" .formatted(escapeJavaString(script.script()))); - sb.append(" // Consider using slice notation instead: [-1:] for last element\n"); } } diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathHelpers.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathHelpers.java index 0c98fc2..3ffce69 100644 --- a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathHelpers.java +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathHelpers.java @@ -151,30 +151,6 @@ public static void evaluateRecursiveDescent(String propertyName, JsonValue curre } } - /// Evaluates recursive descent and then applies subsequent segments. - /// This is a more general version that delegates back to the interpreter for complex cases. - /// @param path the original JsonPath being evaluated - /// @param segmentIndex the index of the recursive descent segment - /// @param current the current value - /// @param root the root document - /// @param results the results list - public static void evaluateRecursiveDescentFull( - JsonPath path, - int segmentIndex, - JsonValue current, - JsonValue root, - List results) { - - // For full recursive descent support, we delegate to the interpreter - // This handles the case where there are segments after the recursive descent - if (path instanceof JsonPathInterpreted interpreted) { - final var ast = interpreted.ast(); - JsonPathInterpreted.evaluateSegments(ast.segments(), segmentIndex, current, root, results); - } else if (path.ast() != null) { - JsonPathInterpreted.evaluateSegments(path.ast().segments(), segmentIndex, current, root, results); - } - } - /// Collects all arrays recursively from a JSON value. /// Used for recursive descent with array index targets like $..book[2]. /// @param current the current JSON value to search @@ -196,24 +172,4 @@ public static void collectArrays(JsonValue current, List arrays) { } } - /// Collects values at a specific property path recursively. - /// Used for recursive descent patterns like $..book. - /// @param propertyName the property name to search for - /// @param current the current JSON value to search - /// @param results the list to collect results into - public static void collectAtPath(String propertyName, JsonValue current, List results) { - if (current instanceof JsonObject obj) { - final var value = obj.members().get(propertyName); - if (value != null) { - results.add(value); - } - for (final var child : obj.members().values()) { - collectAtPath(propertyName, child, results); - } - } else if (current instanceof JsonArray array) { - for (final var element : array.elements()) { - collectAtPath(propertyName, element, results); - } - } - } }