|
| 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 | +} |
0 commit comments