Skip to content

Commit 995d626

Browse files
cursoragentsimbo1905
andcommitted
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<JsonValue> results = compiled.query(json); Co-authored-by: simbo1905 <simbo1905@60hertz.com>
1 parent d3a30bc commit 995d626

File tree

8 files changed

+721
-233
lines changed

8 files changed

+721
-233
lines changed

json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiled.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,4 @@ public List<JsonValue> query(JsonValue json) {
3838
public String toString() {
3939
return originalPath;
4040
}
41-
42-
/// Functional interface for compiled JsonPath executors.
43-
@FunctionalInterface
44-
interface JsonPathExecutor {
45-
/// Executes the compiled JsonPath query against a JSON document.
46-
/// @param current the current node being evaluated
47-
/// @param root the root document (for $ references in filters)
48-
/// @return a list of matching JsonValue instances
49-
List<JsonValue> execute(JsonValue current, JsonValue root);
50-
}
5141
}

json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathCompiler.java

Lines changed: 96 additions & 213 deletions
Large diffs are not rendered by default.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package json.java21.jsonpath;
2+
3+
import jdk.sandbox.java.util.json.JsonValue;
4+
5+
import java.util.List;
6+
7+
/// Functional interface for compiled JsonPath executors.
8+
/// This interface is public to allow generated code to implement it.
9+
@FunctionalInterface
10+
public interface JsonPathExecutor {
11+
/// Executes the compiled JsonPath query against a JSON document.
12+
/// @param current the current node being evaluated
13+
/// @param root the root document (for $ references in filters)
14+
/// @return a list of matching JsonValue instances
15+
List<JsonValue> execute(JsonValue current, JsonValue root);
16+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package json.java21.jsonpath;
2+
3+
import jdk.sandbox.java.util.json.*;
4+
5+
import java.util.List;
6+
7+
/// Public helper methods for runtime-compiled JsonPath executors.
8+
/// This class provides access to internal evaluation methods that generated code needs.
9+
public final class JsonPathHelpers {
10+
11+
private JsonPathHelpers() {
12+
// utility class
13+
}
14+
15+
/// Resolves a property path from a JsonValue.
16+
/// @param current the current JSON value
17+
/// @param props the property path to resolve
18+
/// @return the resolved value, or null if not found
19+
public static JsonValue getPath(JsonValue current, String... props) {
20+
JsonValue value = current;
21+
for (final var prop : props) {
22+
if (value instanceof JsonObject obj) {
23+
value = obj.members().get(prop);
24+
if (value == null) return null;
25+
} else {
26+
return null;
27+
}
28+
}
29+
return value;
30+
}
31+
32+
/// Converts a JsonValue to a comparable object for filter comparisons.
33+
/// @param value the JSON value to convert
34+
/// @return a comparable object (String, Number, Boolean, or null)
35+
public static Object toComparable(JsonValue value) {
36+
if (value == null) return null;
37+
return switch (value) {
38+
case JsonString s -> s.string();
39+
case JsonNumber n -> n.toDouble();
40+
case JsonBoolean b -> b.bool();
41+
case JsonNull ignored -> null;
42+
default -> value;
43+
};
44+
}
45+
46+
/// Compares two values using the specified operator.
47+
/// @param left the left operand
48+
/// @param op the comparison operator name (EQ, NE, LT, LE, GT, GE)
49+
/// @param right the right operand
50+
/// @return true if the comparison is satisfied
51+
@SuppressWarnings("unchecked")
52+
public static boolean compareValues(Object left, String op, Object right) {
53+
if (left == null || right == null) {
54+
return switch (op) {
55+
case "EQ" -> left == right;
56+
case "NE" -> left != right;
57+
default -> false;
58+
};
59+
}
60+
61+
// Numeric comparison
62+
if (left instanceof Number leftNum && right instanceof Number rightNum) {
63+
final double l = leftNum.doubleValue();
64+
final double r = rightNum.doubleValue();
65+
return switch (op) {
66+
case "EQ" -> l == r;
67+
case "NE" -> l != r;
68+
case "LT" -> l < r;
69+
case "LE" -> l <= r;
70+
case "GT" -> l > r;
71+
case "GE" -> l >= r;
72+
default -> false;
73+
};
74+
}
75+
76+
// String comparison
77+
if (left instanceof String && right instanceof String) {
78+
@SuppressWarnings("rawtypes")
79+
final int cmp = ((Comparable) left).compareTo(right);
80+
return switch (op) {
81+
case "EQ" -> cmp == 0;
82+
case "NE" -> cmp != 0;
83+
case "LT" -> cmp < 0;
84+
case "LE" -> cmp <= 0;
85+
case "GT" -> cmp > 0;
86+
case "GE" -> cmp >= 0;
87+
default -> false;
88+
};
89+
}
90+
91+
// Boolean comparison
92+
if (left instanceof Boolean && right instanceof Boolean) {
93+
return switch (op) {
94+
case "EQ" -> left.equals(right);
95+
case "NE" -> !left.equals(right);
96+
default -> false;
97+
};
98+
}
99+
100+
// Fallback equality
101+
return switch (op) {
102+
case "EQ" -> left.equals(right);
103+
case "NE" -> !left.equals(right);
104+
default -> false;
105+
};
106+
}
107+
108+
/// Normalizes an array index (handles negative indices).
109+
/// @param index the index (possibly negative)
110+
/// @param size the array size
111+
/// @return the normalized index
112+
public static int normalizeIdx(int index, int size) {
113+
return index < 0 ? size + index : index;
114+
}
115+
116+
/// Evaluates recursive descent from a starting value.
117+
/// This is used for $.. patterns that need to search all descendants.
118+
/// @param propertyName the property name to search for (null for wildcard)
119+
/// @param current the current value to search from
120+
/// @param results the list to add matching values to
121+
public static void evaluateRecursiveDescent(String propertyName, JsonValue current, List<JsonValue> results) {
122+
// First, try matching at current level
123+
if (propertyName == null) {
124+
// Wildcard - match all children
125+
if (current instanceof JsonObject obj) {
126+
results.addAll(obj.members().values());
127+
for (final var value : obj.members().values()) {
128+
evaluateRecursiveDescent(null, value, results);
129+
}
130+
} else if (current instanceof JsonArray array) {
131+
results.addAll(array.elements());
132+
for (final var element : array.elements()) {
133+
evaluateRecursiveDescent(null, element, results);
134+
}
135+
}
136+
} else {
137+
// Named property - match specific property
138+
if (current instanceof JsonObject obj) {
139+
final var value = obj.members().get(propertyName);
140+
if (value != null) {
141+
results.add(value);
142+
}
143+
for (final var child : obj.members().values()) {
144+
evaluateRecursiveDescent(propertyName, child, results);
145+
}
146+
} else if (current instanceof JsonArray array) {
147+
for (final var element : array.elements()) {
148+
evaluateRecursiveDescent(propertyName, element, results);
149+
}
150+
}
151+
}
152+
}
153+
154+
/// Evaluates recursive descent and then applies subsequent segments.
155+
/// This is a more general version that delegates back to the interpreter for complex cases.
156+
/// @param path the original JsonPath being evaluated
157+
/// @param segmentIndex the index of the recursive descent segment
158+
/// @param current the current value
159+
/// @param root the root document
160+
/// @param results the results list
161+
public static void evaluateRecursiveDescentFull(
162+
JsonPath path,
163+
int segmentIndex,
164+
JsonValue current,
165+
JsonValue root,
166+
List<JsonValue> results) {
167+
168+
// For full recursive descent support, we delegate to the interpreter
169+
// This handles the case where there are segments after the recursive descent
170+
if (path instanceof JsonPathInterpreted interpreted) {
171+
final var ast = interpreted.ast();
172+
JsonPathInterpreted.evaluateSegments(ast.segments(), segmentIndex, current, root, results);
173+
} else if (path.ast() != null) {
174+
JsonPathInterpreted.evaluateSegments(path.ast().segments(), segmentIndex, current, root, results);
175+
}
176+
}
177+
178+
/// Collects all arrays recursively from a JSON value.
179+
/// Used for recursive descent with array index targets like $..book[2].
180+
/// @param current the current JSON value to search
181+
/// @param arrays the list to collect arrays into
182+
public static void collectArrays(JsonValue current, List<JsonValue> arrays) {
183+
if (current instanceof JsonArray array) {
184+
arrays.add(array);
185+
for (final var element : array.elements()) {
186+
collectArrays(element, arrays);
187+
}
188+
} else if (current instanceof JsonObject obj) {
189+
for (final var value : obj.members().values()) {
190+
if (value instanceof JsonArray) {
191+
collectArrays(value, arrays);
192+
} else if (value instanceof JsonObject) {
193+
collectArrays(value, arrays);
194+
}
195+
}
196+
}
197+
}
198+
199+
/// Collects values at a specific property path recursively.
200+
/// Used for recursive descent patterns like $..book.
201+
/// @param propertyName the property name to search for
202+
/// @param current the current JSON value to search
203+
/// @param results the list to collect results into
204+
public static void collectAtPath(String propertyName, JsonValue current, List<JsonValue> results) {
205+
if (current instanceof JsonObject obj) {
206+
final var value = obj.members().get(propertyName);
207+
if (value != null) {
208+
results.add(value);
209+
}
210+
for (final var child : obj.members().values()) {
211+
collectAtPath(propertyName, child, results);
212+
}
213+
} else if (current instanceof JsonArray array) {
214+
for (final var element : array.elements()) {
215+
collectAtPath(propertyName, element, results);
216+
}
217+
}
218+
}
219+
}

json-java21-jsonpath/src/test/java/json/java21/jsonpath/JsonPathAstTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ void testArrayIndex() {
6060
LOG.info(() -> "TEST: testArrayIndex");
6161
final var index = new JsonPathAst.ArrayIndex(0);
6262
assertThat(index.index()).isEqualTo(0);
63-
63+
6464
final var negIndex = new JsonPathAst.ArrayIndex(-1);
6565
assertThat(negIndex.index()).isEqualTo(-1);
6666
}

0 commit comments

Comments
 (0)