From 9b6838d391853acaa8cee57577d5932aa2bc3ae2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:25:29 +0000 Subject: [PATCH 1/5] Issue #128 Make JsonPath an interface Co-authored-by: simbo1905 --- json-java21-jsonpath/README.md | 13 + .../java/json/java21/jsonpath/JsonPath.java | 563 +----------------- .../jsonpath/JsonPathAstInterpreter.java | 530 +++++++++++++++++ .../json/java21/jsonpath/JsonPathAstPath.java | 44 ++ 4 files changed, 604 insertions(+), 546 deletions(-) create mode 100644 json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstInterpreter.java create mode 100644 json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java diff --git a/json-java21-jsonpath/README.md b/json-java21-jsonpath/README.md index f38d36f..4daaae0 100644 --- a/json-java21-jsonpath/README.md +++ b/json-java21-jsonpath/README.md @@ -19,6 +19,19 @@ var titles = JsonPath.parse("$.store.book[*].title").query(doc); var cheap = JsonPath.parse("$.store.book[?(@.price < 10)].title").query(doc); ``` +## Runtime Compilation (Optional) + +`JsonPath.parse(...)` returns a reusable `JsonPath` instance. By default this evaluates by walking an internal AST. +For hot paths, you can optionally request a JDK-compiled evaluator at runtime: + +```java +JsonPath path = JsonPath.parse("$.store.book[*].title").compile(); +var titles = path.query(doc); +``` + +- If the runtime Java compiler is unavailable, `compile()` returns the original (AST-backed) implementation. +- Calling `compile()` on an already-compiled `JsonPath` is a no-op (it returns itself). + ## Syntax At A Glance Operator | Example | What it selects 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..43f06b4 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 @@ -2,7 +2,6 @@ import jdk.sandbox.java.util.json.*; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.logging.Logger; @@ -22,26 +21,20 @@ /// ``` /// /// Based on the JSONPath specification from [...](https://goessner.net/articles/JsonPath/) -public final class JsonPath { +public interface JsonPath { - private static final Logger LOG = Logger.getLogger(JsonPath.class.getName()); - - private final JsonPathAst.Root ast; - - private JsonPath(JsonPathAst.Root ast) { - this.ast = ast; - } + Logger LOG = Logger.getLogger(JsonPath.class.getName()); /// 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) { + 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); + return new JsonPathAstPath(ast); } /// Queries matching values from a JSON document. @@ -52,25 +45,16 @@ 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); - } - - /// Reconstructs the JsonPath expression from the AST. - @Override - public String toString() { - return reconstruct(ast); - } + List query(JsonValue json); /// Evaluates a compiled JsonPath against a JSON document. /// @param path a compiled JsonPath (typically cached) /// @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"); + Objects.requireNonNull(json, "json must not be null"); return path.query(json); } @@ -83,534 +67,21 @@ 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); - } - } - - 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); - } - } - } - - 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; - }; - } - - 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; - } + /// Returns a (potentially) runtime-compiled version of this JsonPath. + /// + /// This method is idempotent: if the receiver is already compiled, it returns itself. + /// Implementations that do not support runtime compilation may return themselves. + JsonPath compile(); - private static String escape(String s) { - return s.replace("'", "\\'"); + /// Returns a (potentially) runtime-compiled version of the provided JsonPath. + static JsonPath compile(JsonPath path) { + Objects.requireNonNull(path, "path must not be null"); + return path.compile(); } } diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstInterpreter.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstInterpreter.java new file mode 100644 index 0000000..a10b48e --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstInterpreter.java @@ -0,0 +1,530 @@ +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; + +/// Package-private interpreter for `JsonPathAst`. +/// +/// This is the original AST-walking implementation extracted from `JsonPath` during the +/// refactor to make `JsonPath` an interface. +final class JsonPathAstInterpreter { + + private static final Logger LOG = Logger.getLogger(JsonPathAstInterpreter.class.getName()); + + private JsonPathAstInterpreter() { + } + + 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); + } + } + + 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); + } + } + } + + 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 -> 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; + }; + } + + 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); + } + } + } + + 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(".."); + 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()); + 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/JsonPathAstPath.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java new file mode 100644 index 0000000..afadf23 --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java @@ -0,0 +1,44 @@ +package json.java21.jsonpath; + +import jdk.sandbox.java.util.json.JsonValue; + +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +/// Package-private AST-backed `JsonPath` implementation. +/// +/// This is the behavior-preserving interpreter that walks `JsonPathAst` at runtime. +final class JsonPathAstPath implements JsonPath { + + private static final Logger LOG = Logger.getLogger(JsonPathAstPath.class.getName()); + + private final JsonPathAst.Root ast; + + JsonPathAstPath(JsonPathAst.Root ast) { + this.ast = Objects.requireNonNull(ast, "ast must not be null"); + } + + @Override + public List query(JsonValue json) { + Objects.requireNonNull(json, "json must not be null"); + LOG.fine(() -> "Querying document with path: " + this); + return JsonPathAstInterpreter.evaluate(ast, json); + } + + @Override + public JsonPath compile() { + // Compiler-backed implementation is added later; keep behavior-preserving no-op for now. + return this; + } + + @Override + public String toString() { + return JsonPathAstInterpreter.reconstruct(ast); + } + + JsonPathAst.Root ast() { + return ast; + } +} + From 4530a3c6cd6d3e84a9d6e144b3bcb8b3c8d8a142 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:33:48 +0000 Subject: [PATCH 2/5] Issue #128 Add runtime-compiled JsonPath Co-authored-by: simbo1905 --- .../java/json/java21/jsonpath/JsonPath.java | 6 +- .../java21/jsonpath/JsonPathAstBacked.java | 10 + .../json/java21/jsonpath/JsonPathAstPath.java | 9 +- .../java21/jsonpath/JsonPathCompiled.java | 6 + .../java21/jsonpath/JsonPathCompiledPath.java | 44 +++ .../java21/jsonpath/JsonPathCompiler.java | 361 ++++++++++++++++++ .../json/java21/jsonpath/JsonPathRuntime.java | 251 ++++++++++++ .../JsonPathRuntimeCompilationTest.java | 124 ++++++ 8 files changed, 802 insertions(+), 9 deletions(-) create mode 100644 json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstBacked.java 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/JsonPathCompiledPath.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/JsonPathRuntime.java create mode 100644 json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathRuntimeCompilationTest.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 43f06b4..1abfd83 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 @@ -77,11 +77,13 @@ static List query(String path, JsonValue json) { /// /// This method is idempotent: if the receiver is already compiled, it returns itself. /// Implementations that do not support runtime compilation may return themselves. - JsonPath compile(); + default JsonPath compile() { + return JsonPathCompiler.compile(this); + } /// Returns a (potentially) runtime-compiled version of the provided JsonPath. static JsonPath compile(JsonPath path) { Objects.requireNonNull(path, "path must not be null"); - return path.compile(); + return JsonPathCompiler.compile(path); } } diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstBacked.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstBacked.java new file mode 100644 index 0000000..e176ac1 --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstBacked.java @@ -0,0 +1,10 @@ +package json.java21.jsonpath; + +/// Marker for `JsonPath` implementations backed by a `JsonPathAst.Root`. +/// +/// This is intentionally package-private so tests in this package can inspect internals, +/// while keeping the public API surface small. +interface JsonPathAstBacked { + JsonPathAst.Root ast(); +} + diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java index afadf23..6df87df 100644 --- a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java @@ -9,7 +9,7 @@ /// Package-private AST-backed `JsonPath` implementation. /// /// This is the behavior-preserving interpreter that walks `JsonPathAst` at runtime. -final class JsonPathAstPath implements JsonPath { +final class JsonPathAstPath implements JsonPath, JsonPathAstBacked { private static final Logger LOG = Logger.getLogger(JsonPathAstPath.class.getName()); @@ -26,17 +26,12 @@ public List query(JsonValue json) { return JsonPathAstInterpreter.evaluate(ast, json); } - @Override - public JsonPath compile() { - // Compiler-backed implementation is added later; keep behavior-preserving no-op for now. - return this; - } - @Override public String toString() { return JsonPathAstInterpreter.reconstruct(ast); } + @Override JsonPathAst.Root ast() { return ast; } 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..18ece2d --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java @@ -0,0 +1,6 @@ +package json.java21.jsonpath; + +/// Marker for `JsonPath` implementations that are already runtime-compiled. +interface JsonPathCompiled extends JsonPath { +} + diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiledPath.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiledPath.java new file mode 100644 index 0000000..24697e4 --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiledPath.java @@ -0,0 +1,44 @@ +package json.java21.jsonpath; + +import jdk.sandbox.java.util.json.JsonValue; + +import java.util.List; +import java.util.Objects; + +/// A `JsonPath` backed by a runtime-compiled generated class. +/// +/// This wrapper intentionally does not retain the AST; it only retains the generated Java source +/// (for diagnostics/debugging) and delegates evaluation to the compiled instance. +final class JsonPathCompiledPath implements JsonPathCompiled { + + private final String expression; + private final String generatedClassName; + private final String javaSource; + private final JsonPath delegate; + + JsonPathCompiledPath(String expression, String generatedClassName, String javaSource, JsonPath delegate) { + this.expression = Objects.requireNonNull(expression, "expression must not be null"); + this.generatedClassName = Objects.requireNonNull(generatedClassName, "generatedClassName must not be null"); + this.javaSource = Objects.requireNonNull(javaSource, "javaSource must not be null"); + this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); + } + + @Override + public List query(JsonValue json) { + return delegate.query(json); + } + + @Override + public String toString() { + return expression; + } + + String generatedClassName() { + return generatedClassName; + } + + String javaSource() { + return javaSource; + } +} + 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..4e2620d --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java @@ -0,0 +1,361 @@ +package json.java21.jsonpath; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +/// Package-private runtime compiler for `JsonPath`. +/// +/// This compiles AST-backed JsonPath instances into generated Java classes using the JDK compiler tools. +final class JsonPathCompiler { + + private static final Logger LOG = Logger.getLogger(JsonPathCompiler.class.getName()); + private static final AtomicInteger COUNTER = new AtomicInteger(); + private static final String PACKAGE_NAME = "json.java21.jsonpath"; + + private JsonPathCompiler() { + } + + static JsonPath compile(JsonPath path) { + Objects.requireNonNull(path, "path must not be null"); + if (path instanceof JsonPathCompiled) { + return path; + } + if (path instanceof JsonPathAstBacked astBacked) { + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + LOG.info(() -> "Runtime compiler unavailable; returning AST-backed JsonPath for: " + path); + return path; + } + return compileAst(compiler, astBacked.ast()); + } + return path; + } + + static String toJavaSourceForTests(JsonPathAst.Root ast) { + final var className = "JsonPathGenerated_TEST"; + final var expression = JsonPathAstInterpreter.reconstruct(ast); + return generateSource(PACKAGE_NAME, className, expression, ast); + } + + private static JsonPath compileAst(JavaCompiler compiler, JsonPathAst.Root ast) { + final var expression = JsonPathAstInterpreter.reconstruct(ast); + final var className = "JsonPathGenerated_" + COUNTER.incrementAndGet(); + final var fqcn = PACKAGE_NAME + "." + className; + + final var javaSource = generateSource(PACKAGE_NAME, className, expression, ast); + final var bytecode = compileToBytes(compiler, fqcn, javaSource); + final var instance = instantiate(fqcn, bytecode); + return new JsonPathCompiledPath(expression, fqcn, javaSource, instance); + } + + private static JsonPath instantiate(String fqcn, Map bytecode) { + try { + final var loader = new InMemoryClassLoader(JsonPathCompiler.class.getClassLoader(), bytecode); + final Class clazz = loader.loadClass(fqcn); + if (!JsonPath.class.isAssignableFrom(clazz)) { + throw new IllegalStateException("Generated class does not implement JsonPath: " + fqcn); + } + @SuppressWarnings("unchecked") + final Class typed = (Class) clazz; + final Constructor ctor = typed.getDeclaredConstructor(); + ctor.setAccessible(true); + return ctor.newInstance(); + } catch (ReflectiveOperationException ex) { + throw new IllegalStateException("Failed to load/instantiate generated JsonPath class: " + fqcn, ex); + } + } + + private static Map compileToBytes(JavaCompiler compiler, String fqcn, String javaSource) { + final var diagnostics = new DiagnosticCollector(); + + try (StandardJavaFileManager standard = compiler.getStandardFileManager(diagnostics, null, null)) { + final var memManager = new MemoryFileManager(standard); + + final var classpath = System.getProperty("java.class.path"); + final List options = new ArrayList<>(); + options.add("--release"); + options.add("21"); + if (classpath != null && !classpath.isBlank()) { + options.add("-classpath"); + options.add(classpath); + } + options.add("-Xlint:none"); + + final var sources = List.of(new StringJavaFileObject(fqcn, javaSource)); + final var task = compiler.getTask(null, memManager, diagnostics, options, null, sources); + final Boolean ok = task.call(); + if (!Boolean.TRUE.equals(ok)) { + throw new IllegalStateException("Failed to compile generated JsonPath:\n" + formatDiagnostics(diagnostics, javaSource)); + } + + return memManager.bytecode(); + } catch (IOException ex) { + throw new IllegalStateException("Failed compiling generated JsonPath source for: " + fqcn, ex); + } + } + + private static String formatDiagnostics(DiagnosticCollector diagnostics, String javaSource) { + final var sb = new StringBuilder(); + for (Diagnostic d : diagnostics.getDiagnostics()) { + sb.append(d.getKind()) + .append(" at ") + .append(d.getLineNumber()) + .append(":") + .append(d.getColumnNumber()) + .append(" ") + .append(d.getMessage(null)) + .append("\n"); + } + sb.append("\n--- Generated Source ---\n").append(javaSource).append("\n"); + return sb.toString(); + } + + private static String generateSource(String pkg, String className, String expression, JsonPathAst.Root ast) { + final var sb = new StringBuilder(8_192); + sb.append("package ").append(pkg).append(";\n\n"); + sb.append("import jdk.sandbox.java.util.json.*;\n"); + sb.append("import java.util.*;\n"); + sb.append("import java.util.stream.*;\n\n"); + sb.append("final class ").append(className).append(" implements JsonPath {\n"); + sb.append(" public ").append(className).append("() {}\n\n"); + sb.append(" @Override public List query(JsonValue json) {\n"); + sb.append(" Objects.requireNonNull(json, \"json must not be null\");\n"); + sb.append(" Stream s = Stream.of(json);\n"); + + for (final var segment : ast.segments()) { + emitSegment(sb, segment); + } + + sb.append(" return s.toList();\n"); + sb.append(" }\n\n"); + sb.append(" @Override public String toString() { return ").append(javaStringLiteral(expression)).append("; }\n"); + sb.append("}\n"); + return sb.toString(); + } + + private static void emitSegment(StringBuilder sb, JsonPathAst.Segment segment) { + switch (segment) { + case JsonPathAst.PropertyAccess prop -> + sb.append(" s = s.flatMap(v -> JsonPathRuntime.selectProperty(v, ").append(javaStringLiteral(prop.name())).append("));\n"); + case JsonPathAst.ArrayIndex arr -> + sb.append(" s = s.flatMap(v -> JsonPathRuntime.selectIndex(v, ").append(arr.index()).append("));\n"); + case JsonPathAst.ArraySlice slice -> { + sb.append(" s = s.flatMap(v -> JsonPathRuntime.selectSlice(v, ") + .append(slice.start() == null ? "null" : "Integer.valueOf(" + slice.start() + ")") + .append(", ") + .append(slice.end() == null ? "null" : "Integer.valueOf(" + slice.end() + ")") + .append(", ") + .append(slice.step() == null ? "null" : "Integer.valueOf(" + slice.step() + ")") + .append("));\n"); + } + case JsonPathAst.Wildcard ignored -> + sb.append(" s = s.flatMap(JsonPathRuntime::selectWildcard);\n"); + case JsonPathAst.RecursiveDescent desc -> emitRecursive(sb, desc); + case JsonPathAst.Filter filter -> { + sb.append(" s = s.flatMap(v -> JsonPathRuntime.filterArray(v, e -> "); + sb.append(emitFilterExpression(filter.expression(), "e")); + sb.append("));\n"); + } + case JsonPathAst.Union union -> emitUnion(sb, union); + case JsonPathAst.ScriptExpression script -> + sb.append(" s = s.flatMap(v -> JsonPathRuntime.selectScript(v, ").append(javaStringLiteral(script.script())).append("));\n"); + } + } + + private static void emitRecursive(StringBuilder sb, JsonPathAst.RecursiveDescent desc) { + switch (desc.target()) { + case JsonPathAst.PropertyAccess prop -> + sb.append(" s = s.flatMap(v -> JsonPathRuntime.selectRecursiveProperty(v, ").append(javaStringLiteral(prop.name())).append("));\n"); + case JsonPathAst.Wildcard ignored -> + sb.append(" s = s.flatMap(JsonPathRuntime::selectRecursiveWildcard);\n"); + default -> + sb.append(" s = Stream.empty();\n"); + } + } + + private static void emitUnion(StringBuilder sb, JsonPathAst.Union union) { + final var selectors = union.selectors(); + final var first = selectors.getFirst(); + if (first instanceof JsonPathAst.PropertyAccess) { + sb.append(" s = s.flatMap(v -> JsonPathRuntime.selectUnionProperties(v, new String[]{"); + for (int i = 0; i < selectors.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(javaStringLiteral(((JsonPathAst.PropertyAccess) selectors.get(i)).name())); + } + sb.append("}));\n"); + return; + } + if (first instanceof JsonPathAst.ArrayIndex) { + sb.append(" s = s.flatMap(v -> JsonPathRuntime.selectUnionIndices(v, new int[]{"); + for (int i = 0; i < selectors.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(((JsonPathAst.ArrayIndex) selectors.get(i)).index()); + } + sb.append("}));\n"); + return; + } + sb.append(" s = Stream.empty();\n"); + } + + private static String emitFilterExpression(JsonPathAst.FilterExpression expr, String currentVar) { + return switch (expr) { + case JsonPathAst.ExistsFilter exists -> "JsonPathRuntime.resolvePropertyPath(" + currentVar + ", new String[]{" + + joinStringLiterals(exists.path().properties()) + "}) != null"; + case JsonPathAst.ComparisonFilter comp -> "JsonPathRuntime.compareComparable(" + + emitFilterOperand(comp.left(), currentVar) + ", JsonPathAst.ComparisonOp." + comp.op().name() + ", " + + emitFilterOperand(comp.right(), currentVar) + ")"; + case JsonPathAst.LogicalFilter logical -> switch (logical.op()) { + case NOT -> "(!(" + emitFilterExpression(logical.left(), currentVar) + "))"; + case AND -> "((" + emitFilterExpression(logical.left(), currentVar) + ") && (" + emitFilterExpression(logical.right(), currentVar) + "))"; + case OR -> "((" + emitFilterExpression(logical.left(), currentVar) + ") || (" + emitFilterExpression(logical.right(), currentVar) + "))"; + }; + case JsonPathAst.CurrentNode ignored -> "true"; + case JsonPathAst.PropertyPath path -> "JsonPathRuntime.resolvePropertyPath(" + currentVar + ", new String[]{" + + joinStringLiterals(path.properties()) + "}) != null"; + case JsonPathAst.LiteralValue ignored -> "true"; + }; + } + + private static String emitFilterOperand(JsonPathAst.FilterExpression expr, String currentVar) { + return switch (expr) { + case JsonPathAst.PropertyPath path -> "JsonPathRuntime.toComparable(JsonPathRuntime.resolvePropertyPath(" + currentVar + ", new String[]{" + + joinStringLiterals(path.properties()) + "}))"; + case JsonPathAst.LiteralValue lit -> literalToJava(lit.value()); + case JsonPathAst.CurrentNode ignored -> "JsonPathRuntime.toComparable(" + currentVar + ")"; + default -> "null"; + }; + } + + private static String joinStringLiterals(List strings) { + final var sb = new StringBuilder(); + for (int i = 0; i < strings.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(javaStringLiteral(strings.get(i))); + } + return sb.toString(); + } + + private static String literalToJava(Object value) { + if (value == null) return "null"; + if (value instanceof String s) return javaStringLiteral(s); + if (value instanceof Boolean b) return b ? "true" : "false"; + if (value instanceof Integer i) return Integer.toString(i); + if (value instanceof Long l) return l + "L"; + if (value instanceof Double d) return Double.toString(d) + "d"; + if (value instanceof Float f) return Float.toString(f) + "f"; + if (value instanceof Number n) return n.toString(); + // Fallback: should not occur for filter literals + return javaStringLiteral(String.valueOf(value)); + } + + private static String javaStringLiteral(String s) { + return "\"" + escapeJava(s) + "\""; + } + + private static String escapeJava(String s) { + final var out = new StringBuilder(s.length() + 16); + for (int i = 0; i < s.length(); i++) { + final char c = s.charAt(i); + switch (c) { + case '\\' -> out.append("\\\\"); + case '"' -> out.append("\\\""); + case '\n' -> out.append("\\n"); + case '\r' -> out.append("\\r"); + case '\t' -> out.append("\\t"); + default -> out.append(c); + } + } + return out.toString(); + } + + private static final class StringJavaFileObject extends SimpleJavaFileObject { + private final String source; + + StringJavaFileObject(String className, String source) { + super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.source = source; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return source; + } + } + + private static final class BytecodeJavaFileObject extends SimpleJavaFileObject { + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + BytecodeJavaFileObject(String className, Kind kind) { + super(URI.create("mem:///" + className.replace('.', '/') + kind.extension), kind); + } + + @Override + public OutputStream openOutputStream() { + return out; + } + + byte[] bytes() { + return out.toByteArray(); + } + } + + private static final class MemoryFileManager extends ForwardingJavaFileManager { + private final Map compiled = new ConcurrentHashMap<>(); + + MemoryFileManager(StandardJavaFileManager fileManager) { + super(fileManager); + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) { + final var file = new BytecodeJavaFileObject(className, kind); + compiled.put(className, file); + return file; + } + + Map bytecode() { + final var out = new ConcurrentHashMap(); + compiled.forEach((k, v) -> out.put(k, v.bytes())); + return out; + } + } + + private static final class InMemoryClassLoader extends ClassLoader { + private final Map bytecode; + + InMemoryClassLoader(ClassLoader parent, Map bytecode) { + super(parent); + this.bytecode = bytecode; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + final var bytes = bytecode.get(name); + if (bytes == null) { + return super.findClass(name); + } + return defineClass(name, bytes, 0, bytes.length); + } + } +} + diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathRuntime.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathRuntime.java new file mode 100644 index 0000000..0921fc1 --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathRuntime.java @@ -0,0 +1,251 @@ +package json.java21.jsonpath; + +import jdk.sandbox.java.util.json.*; + +import java.util.Objects; +import java.util.function.Predicate; +import java.util.logging.Logger; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/// Runtime helpers used by generated, compiler-backed JsonPath implementations. +/// +/// These helpers are package-private so generated code can call them without expanding the public API. +final class JsonPathRuntime { + + private static final Logger LOG = Logger.getLogger(JsonPathRuntime.class.getName()); + + private JsonPathRuntime() { + } + + static Stream selectProperty(JsonValue v, String name) { + if (v instanceof JsonObject obj) { + final var value = obj.members().get(name); + return value == null ? Stream.empty() : Stream.of(value); + } + return Stream.empty(); + } + + static Stream selectIndex(JsonValue v, int index) { + if (v instanceof JsonArray array) { + final var elements = array.elements(); + int idx = index; + if (idx < 0) { + idx = elements.size() + idx; + } + if (idx >= 0 && idx < elements.size()) { + return Stream.of(elements.get(idx)); + } + } + return Stream.empty(); + } + + static Stream selectSlice(JsonValue v, Integer start, Integer end, Integer step) { + if (!(v instanceof JsonArray array)) { + return Stream.empty(); + } + + final var elements = array.elements(); + final int size = elements.size(); + final int actualStep = step != null ? step : 1; + if (actualStep == 0) { + return Stream.empty(); + } + + if (actualStep > 0) { + int s = start != null ? normalizeIndex(start, size) : 0; + int e = end != null ? normalizeIndex(end, size) : size; + + s = Math.max(0, Math.min(s, size)); + e = Math.max(0, Math.min(e, size)); + + return IntStream.iterate(s, i -> i < e, i -> i + actualStep) + .mapToObj(elements::get); + } + + int s = start != null ? normalizeIndex(start, size) : size - 1; + final int e = end != null ? normalizeIndex(end, size) : -1; + s = Math.max(0, Math.min(s, size - 1)); + + return IntStream.iterate(s, i -> i > e, i -> i + actualStep) + .mapToObj(elements::get); + } + + static Stream selectWildcard(JsonValue v) { + if (v instanceof JsonObject obj) { + return obj.members().values().stream(); + } + if (v instanceof JsonArray array) { + return array.elements().stream(); + } + return Stream.empty(); + } + + static Stream selectUnionProperties(JsonValue v, String[] names) { + Objects.requireNonNull(names, "names must not be null"); + if (!(v instanceof JsonObject obj)) { + return Stream.empty(); + } + return Stream.of(names) + .map(n -> obj.members().get(n)) + .filter(Objects::nonNull); + } + + static Stream selectUnionIndices(JsonValue v, int[] indices) { + Objects.requireNonNull(indices, "indices must not be null"); + if (!(v instanceof JsonArray array)) { + return Stream.empty(); + } + final var elements = array.elements(); + return IntStream.of(indices) + .map(i -> i < 0 ? elements.size() + i : i) + .filter(i -> i >= 0 && i < elements.size()) + .mapToObj(elements::get); + } + + static Stream selectScript(JsonValue v, String script) { + Objects.requireNonNull(script, "script must not be null"); + if (!(v instanceof JsonArray array)) { + return Stream.empty(); + } + final var scriptText = script.trim(); + if (scriptText.equals("@.length-1")) { + final int lastIndex = array.elements().size() - 1; + if (lastIndex >= 0) { + return Stream.of(array.elements().get(lastIndex)); + } + return Stream.empty(); + } + LOG.warning(() -> "Unsupported script expression: " + scriptText); + return Stream.empty(); + } + + static Stream filterArray(JsonValue v, Predicate predicate) { + Objects.requireNonNull(predicate, "predicate must not be null"); + if (v instanceof JsonArray array) { + return array.elements().stream().filter(predicate); + } + return Stream.empty(); + } + + static Stream selectRecursiveProperty(JsonValue v, String name) { + final var b = Stream.builder(); + walkRecursive(v, node -> { + if (node instanceof JsonObject obj) { + final var value = obj.members().get(name); + if (value != null) { + b.add(value); + } + } + }); + return b.build(); + } + + static Stream selectRecursiveWildcard(JsonValue v) { + final var b = Stream.builder(); + walkRecursive(v, node -> { + if (node instanceof JsonObject obj) { + obj.members().values().forEach(b::add); + } else if (node instanceof JsonArray array) { + array.elements().forEach(b::add); + } + }); + return b.build(); + } + + private static void walkRecursive(JsonValue v, java.util.function.Consumer atNode) { + atNode.accept(v); + if (v instanceof JsonObject obj) { + obj.members().values().forEach(child -> walkRecursive(child, atNode)); + } else if (v instanceof JsonArray array) { + array.elements().forEach(child -> walkRecursive(child, atNode)); + } + } + + static JsonValue resolvePropertyPath(JsonValue current, String[] properties) { + Objects.requireNonNull(properties, "properties must not be null"); + JsonValue value = current; + for (final var prop : properties) { + if (value instanceof JsonObject obj) { + value = obj.members().get(prop); + if (value == null) { + return null; + } + } else { + return null; + } + } + return value; + } + + 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; + }; + } + + @SuppressWarnings("unchecked") + static boolean compareComparable(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; + }; + } + + 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; + }; + } + 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; + }; + } + case Boolean ignored when right instanceof Boolean -> { + return switch (op) { + case EQ -> left.equals(right); + case NE -> !left.equals(right); + default -> false; + }; + } + default -> { + } + } + + return switch (op) { + case EQ -> left.equals(right); + case NE -> !left.equals(right); + default -> false; + }; + } + + private static int normalizeIndex(int index, int size) { + if (index < 0) { + return size + index; + } + return index; + } +} + diff --git a/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathRuntimeCompilationTest.java b/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathRuntimeCompilationTest.java new file mode 100644 index 0000000..14d46c1 --- /dev/null +++ b/json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathRuntimeCompilationTest.java @@ -0,0 +1,124 @@ +package json.java21.jsonpath; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonValue; +import org.junit.jupiter.api.Test; + +import javax.tools.ToolProvider; +import java.util.List; +import java.util.logging.Logger; + +import static org.assertj.core.api.Assertions.assertThat; + +class JsonPathRuntimeCompilationTest extends JsonPathLoggingConfig { + + private static final Logger LOG = Logger.getLogger(JsonPathRuntimeCompilationTest.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 + } + } + } + """; + + @Test + void testCompileIdempotent() { + LOG.info(() -> "TEST: testCompileIdempotent"); + + final var ast = JsonPath.parse("$.store.book[*].author"); + final var compiled = ast.compile(); + + assertThat(compiled.compile()).isSameAs(compiled); + assertThat(compiled.toString()).isEqualTo(ast.toString()); + + final boolean canCompile = ToolProvider.getSystemJavaCompiler() != null; + if (canCompile) { + assertThat(compiled).isInstanceOf(JsonPathCompiled.class); + } else { + assertThat(compiled).isSameAs(ast); + } + } + + @Test + void testCompiledMatchesAstForRepresentativeExpressions() { + LOG.info(() -> "TEST: testCompiledMatchesAstForRepresentativeExpressions"); + + final JsonValue doc = Json.parse(STORE_JSON); + + final List expressions = List.of( + "$", + "$.store", + "$.store.bicycle.color", + "$.store.book[*].author", + "$..price", + "$..book[2].title", + "$..book[-1:]", + "$..book[0,1]", + "$..book[:2]", + "$..book[?(@.isbn)]", + "$..book[?(@.price<10)].title", + "$.store['book','bicycle']", + "$..*", + "$..book[(@.length-1)].title", + "$.store.book[::-1].title" + ); + + for (final var expr : expressions) { + final var ast = JsonPath.parse(expr); + final var compiled = ast.compile(); + + final var expected = ast.query(doc); + final var actual = compiled.query(doc); + + assertThat(actual) + .as("compiled query results match AST for %s", expr) + .isEqualTo(expected); + } + } + + @Test + void testGeneratedSourceEmitsLogicalOperators() { + LOG.info(() -> "TEST: testGeneratedSourceEmitsLogicalOperators"); + + final var ast = JsonPathParser.parse("$[?(@.a == true && (@.b == true || @.c == true))]"); + final var src = JsonPathCompiler.toJavaSourceForTests(ast); + + assertThat(src).contains("&&"); + assertThat(src).contains("||"); + assertThat(src).contains("JsonPathRuntime.compareComparable"); + assertThat(src).contains("JsonPathRuntime.resolvePropertyPath"); + } +} + From 158fd122755b3e2abe1be730b065ee1fd168985a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:50:48 +0000 Subject: [PATCH 3/5] Issue #128 Fix JsonPathAstBacked method visibility Co-authored-by: simbo1905 --- .../src/main/java/json/java21/jsonpath/JsonPathAstPath.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java index 6df87df..3f2c957 100644 --- a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathAstPath.java @@ -32,7 +32,7 @@ public String toString() { } @Override - JsonPathAst.Root ast() { + public JsonPathAst.Root ast() { return ast; } } From f82b0ed9f2626420a60769afd2041bb6ce83d606 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:51:34 +0000 Subject: [PATCH 4/5] Issue #128 Fix JsonPathRuntime slice lambdas Co-authored-by: simbo1905 --- .../main/java/json/java21/jsonpath/JsonPathRuntime.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathRuntime.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathRuntime.java index 0921fc1..2f1a2d3 100644 --- a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathRuntime.java +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathRuntime.java @@ -59,7 +59,9 @@ static Stream selectSlice(JsonValue v, Integer start, Integer end, In s = Math.max(0, Math.min(s, size)); e = Math.max(0, Math.min(e, size)); - return IntStream.iterate(s, i -> i < e, i -> i + actualStep) + final int sFinal = s; + final int eFinal = e; + return IntStream.iterate(sFinal, i -> i < eFinal, i -> i + actualStep) .mapToObj(elements::get); } @@ -67,7 +69,8 @@ static Stream selectSlice(JsonValue v, Integer start, Integer end, In final int e = end != null ? normalizeIndex(end, size) : -1; s = Math.max(0, Math.min(s, size - 1)); - return IntStream.iterate(s, i -> i > e, i -> i + actualStep) + final int sFinal = s; + return IntStream.iterate(sFinal, i -> i > e, i -> i + actualStep) .mapToObj(elements::get); } From 34142943c4c46019e358642d3bb050eb1669e3c6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:52:42 +0000 Subject: [PATCH 5/5] Issue #128 Define generated JsonPath in same loader Co-authored-by: simbo1905 --- .../java21/jsonpath/JsonPathCompiler.java | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 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 4e2620d..c3cbc6b 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 @@ -14,6 +14,7 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Constructor; +import java.lang.invoke.MethodHandles; import java.net.URI; import java.util.ArrayList; import java.util.List; @@ -70,8 +71,7 @@ private static JsonPath compileAst(JavaCompiler compiler, JsonPathAst.Root ast) private static JsonPath instantiate(String fqcn, Map bytecode) { try { - final var loader = new InMemoryClassLoader(JsonPathCompiler.class.getClassLoader(), bytecode); - final Class clazz = loader.loadClass(fqcn); + final Class clazz = defineInThisLoader(fqcn, bytecode); if (!JsonPath.class.isAssignableFrom(clazz)) { throw new IllegalStateException("Generated class does not implement JsonPath: " + fqcn); } @@ -85,6 +85,30 @@ private static JsonPath instantiate(String fqcn, Map bytecode) { } } + private static Class defineInThisLoader(String fqcn, Map bytecode) { + final var bytes = bytecode.get(fqcn); + if (bytes == null) { + throw new IllegalStateException("Missing bytecode for generated class: " + fqcn); + } + try { + // Define into the same loader/package as JsonPathCompiler so package-private helpers are accessible. + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + // Define any nested classes first, then the main (or vice versa) doesn't matter for our current codegen. + for (final var entry : bytecode.entrySet()) { + if (!entry.getKey().equals(fqcn)) { + try { + lookup.defineClass(entry.getValue()); + } catch (LinkageError ignored) { + // Best-effort: might already be defined in this JVM. + } + } + } + return lookup.defineClass(bytes); + } catch (IllegalAccessException ex) { + throw new IllegalStateException("Failed to define generated class in current loader: " + fqcn, ex); + } + } + private static Map compileToBytes(JavaCompiler compiler, String fqcn, String javaSource) { final var diagnostics = new DiagnosticCollector(); @@ -340,22 +364,5 @@ Map bytecode() { } } - private static final class InMemoryClassLoader extends ClassLoader { - private final Map bytecode; - - InMemoryClassLoader(ClassLoader parent, Map bytecode) { - super(parent); - this.bytecode = bytecode; - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - final var bytes = bytecode.get(name); - if (bytes == null) { - return super.findClass(name); - } - return defineClass(name, bytes, 0, bytes.length); - } - } }