001package com.streamconverter.command.impl.json; 002 003import com.fasterxml.jackson.core.JsonFactory; 004import com.fasterxml.jackson.core.JsonGenerator; 005import com.fasterxml.jackson.core.JsonParser; 006import com.fasterxml.jackson.core.JsonToken; 007import com.streamconverter.command.AbstractStreamCommand; 008import com.streamconverter.path.IPath; 009import java.io.IOException; 010import java.io.InputStream; 011import java.io.OutputStream; 012import java.util.ArrayList; 013import java.util.List; 014 015/** 016 * JSON Filter Command Class 017 * 018 * <p>This class implements pure data extraction from JSON using TreePath expressions. Unlike 019 * JsonNavigateCommand which applies transformations, JsonFilterCommand only extracts/filters data 020 * based on specified paths without any modifications. 021 * 022 * <p>Features: - Extract specific elements using TreePath expressions - Preserve exact data types 023 * and structure of extracted elements - Streaming processing via Jackson Streaming API 024 * (JsonParser/JsonGenerator); the document is never fully loaded into memory - Support for simple 025 * path expressions including wildcards ($[*].field, $.array[*].nested.field) 026 */ 027public class JsonFilterCommand extends AbstractStreamCommand { 028 029 private final IPath<List<String>> jsonPath; 030 private final JsonFactory jsonFactory; 031 032 /** 033 * Constructor for JSON filtering with typed TreePath selector. 034 * 035 * @param jsonPath the typed TreePath to extract data 036 * @throws IllegalArgumentException if jsonPath is null 037 */ 038 private JsonFilterCommand(IPath<List<String>> jsonPath) { 039 this.jsonPath = jsonPath; 040 this.jsonFactory = new JsonFactory(); 041 } 042 043 /** 044 * Factory method for JSON filtering with typed path selector. 045 * 046 * @param jsonPath the typed path to extract data 047 * @return a JsonFilterCommand instance 048 * @throws IllegalArgumentException if jsonPath is null 049 */ 050 public static JsonFilterCommand create(IPath<List<String>> jsonPath) { 051 if (jsonPath == null) { 052 throw new IllegalArgumentException("TreePath cannot be null"); 053 } 054 return new JsonFilterCommand(jsonPath); 055 } 056 057 @Override 058 public void execute(InputStream inputStream, OutputStream outputStream) throws IOException { 059 String path = jsonPath.toString(); 060 List<PathSegment> segments = parsePath(path); 061 062 try (JsonParser parser = jsonFactory.createParser(inputStream); 063 JsonGenerator generator = jsonFactory.createGenerator(outputStream)) { 064 generator.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); 065 066 if (segments.isEmpty()) { 067 // Root path "$": copy the entire document 068 copyValue(parser, generator); 069 } else { 070 extractPath(parser, generator, segments, 0); 071 } 072 073 generator.flush(); 074 } 075 } 076 077 // ------------------------------------------------------------------------- 078 // Path parsing 079 // ------------------------------------------------------------------------- 080 081 /** A single segment in a parsed path. */ 082 private static class PathSegment { 083 084 final String field; // non-null for field access 085 final boolean wildcard; // true for [*] 086 final int index; // >= 0 for numeric index, -1 otherwise 087 088 static PathSegment field(String name) { 089 return new PathSegment(name, false, -1); 090 } 091 092 static PathSegment wildcard() { 093 return new PathSegment(null, true, -1); 094 } 095 096 static PathSegment index(int i) { 097 return new PathSegment(null, false, i); 098 } 099 100 private PathSegment(String field, boolean wildcard, int index) { 101 this.field = field; 102 this.wildcard = wildcard; 103 this.index = index; 104 } 105 106 boolean isField() { 107 return field != null; 108 } 109 } 110 111 /** 112 * Parse a path string into an ordered list of {@link PathSegment}s. 113 * 114 * <p>Supported syntax: 115 * 116 * <ul> 117 * <li>{@code $} – root (empty list) 118 * <li>{@code $.name} – top-level field 119 * <li>{@code $[*].name} – root-array wildcard then field 120 * <li>{@code $.users[*].profile.department} – field, wildcard, nested fields 121 * </ul> 122 * 123 * @param path the path expression 124 * @return ordered list of path segments (empty = root) 125 */ 126 private static List<PathSegment> parsePath(String path) { 127 List<PathSegment> result = new ArrayList<>(); 128 if ("$".equals(path)) { 129 return result; 130 } 131 132 // Strip leading "$" then process the remaining characters 133 int start = path.startsWith("$") ? 1 : 0; 134 String rest = path.substring(start); 135 136 int i = 0; 137 while (i < rest.length()) { 138 char c = rest.charAt(i); 139 if (c == '.') { 140 i++; // skip dot separator 141 } else if (c == '[') { 142 // Array access: [*] or [N] 143 int close = rest.indexOf(']', i); 144 if (close == -1) { 145 break; // malformed – stop here 146 } 147 String inner = rest.substring(i + 1, close); 148 if ("*".equals(inner)) { 149 result.add(PathSegment.wildcard()); 150 } else { 151 try { 152 result.add(PathSegment.index(Integer.parseInt(inner))); 153 } catch (NumberFormatException e) { 154 result.add(PathSegment.wildcard()); // treat unknown as wildcard 155 } 156 } 157 i = close + 1; 158 } else { 159 // Field name: read until '.', '[', or end 160 int end = i; 161 while (end < rest.length() && rest.charAt(end) != '.' && rest.charAt(end) != '[') { 162 end++; 163 } 164 String fieldName = rest.substring(i, end); 165 if (!fieldName.isEmpty()) { 166 result.add(PathSegment.field(fieldName)); 167 } 168 i = end; 169 } 170 } 171 return result; 172 } 173 174 // ------------------------------------------------------------------------- 175 // Streaming extraction 176 // ------------------------------------------------------------------------- 177 178 /** 179 * Advance the parser to the next value and recursively navigate {@code segments[segIdx..]} to 180 * write the matching value(s) to {@code generator}. 181 * 182 * @param parser the JSON parser (not yet advanced to the target value) 183 * @param generator the JSON generator 184 * @param segments the full path segment list 185 * @param segIdx current position in {@code segments} 186 */ 187 private void extractPath( 188 JsonParser parser, JsonGenerator generator, List<PathSegment> segments, int segIdx) 189 throws IOException { 190 191 if (segIdx >= segments.size()) { 192 copyValue(parser, generator); 193 return; 194 } 195 196 PathSegment seg = segments.get(segIdx); 197 JsonToken token = parser.nextToken(); 198 199 if (token == null) { 200 generator.writeNull(); 201 return; 202 } 203 204 if (seg.isField()) { 205 // Expect an object; find the named field 206 if (token != JsonToken.START_OBJECT) { 207 skipValue(parser, token); 208 generator.writeNull(); 209 return; 210 } 211 boolean found = false; 212 JsonToken t; 213 while ((t = parser.nextToken()) != null && t != JsonToken.END_OBJECT) { 214 String name = parser.currentName(); 215 if (seg.field.equals(name)) { 216 extractPath(parser, generator, segments, segIdx + 1); 217 found = true; 218 } else { 219 parser.nextToken(); 220 skipValue(parser, parser.currentToken()); 221 } 222 } 223 if (!found) { 224 generator.writeNull(); 225 } 226 } else if (seg.wildcard) { 227 // Expect an array; iterate elements and extract from each 228 if (token != JsonToken.START_ARRAY) { 229 skipValue(parser, token); 230 generator.writeNull(); 231 return; 232 } 233 generator.writeStartArray(); 234 JsonToken elemToken; 235 while ((elemToken = parser.nextToken()) != null && elemToken != JsonToken.END_ARRAY) { 236 if (segIdx + 1 >= segments.size()) { 237 copyValue(parser, generator, elemToken); 238 } else { 239 extractFromToken(parser, generator, segments, segIdx + 1, elemToken); 240 } 241 } 242 generator.writeEndArray(); 243 } else { 244 // Numeric index access 245 if (token != JsonToken.START_ARRAY) { 246 skipValue(parser, token); 247 generator.writeNull(); 248 return; 249 } 250 int currentIdx = 0; 251 boolean found = false; 252 JsonToken arrToken; 253 while ((arrToken = parser.nextToken()) != null && arrToken != JsonToken.END_ARRAY) { 254 if (currentIdx == seg.index) { 255 extractPath(parser, generator, segments, segIdx + 1); 256 found = true; 257 JsonToken skipToken; 258 while ((skipToken = parser.nextToken()) != null && skipToken != JsonToken.END_ARRAY) { 259 skipValue(parser, skipToken); 260 } 261 break; 262 } else { 263 skipValue(parser, arrToken); 264 } 265 currentIdx++; 266 } 267 if (!found) { 268 generator.writeNull(); 269 } 270 } 271 } 272 273 /** 274 * Same as {@link #extractPath} but the parser's current token has already been consumed and is 275 * supplied as {@code currentToken}. Used when iterating array elements where {@code nextToken()} 276 * was already called to detect {@code END_ARRAY}. 277 */ 278 private void extractFromToken( 279 JsonParser parser, 280 JsonGenerator generator, 281 List<PathSegment> segments, 282 int segIdx, 283 JsonToken currentToken) 284 throws IOException { 285 286 if (segIdx >= segments.size()) { 287 copyValue(parser, generator, currentToken); 288 return; 289 } 290 291 PathSegment seg = segments.get(segIdx); 292 293 if (seg.isField()) { 294 if (currentToken != JsonToken.START_OBJECT) { 295 skipValue(parser, currentToken); 296 generator.writeNull(); 297 return; 298 } 299 boolean found = false; 300 JsonToken t2; 301 while ((t2 = parser.nextToken()) != null && t2 != JsonToken.END_OBJECT) { 302 String name = parser.currentName(); 303 if (seg.field.equals(name)) { 304 extractPath(parser, generator, segments, segIdx + 1); 305 found = true; 306 } else { 307 parser.nextToken(); 308 skipValue(parser, parser.currentToken()); 309 } 310 } 311 if (!found) { 312 generator.writeNull(); 313 } 314 } else if (seg.wildcard) { 315 if (currentToken != JsonToken.START_ARRAY) { 316 skipValue(parser, currentToken); 317 generator.writeNull(); 318 return; 319 } 320 generator.writeStartArray(); 321 JsonToken elemToken2; 322 while ((elemToken2 = parser.nextToken()) != null && elemToken2 != JsonToken.END_ARRAY) { 323 if (segIdx + 1 >= segments.size()) { 324 copyValue(parser, generator, elemToken2); 325 } else { 326 extractFromToken(parser, generator, segments, segIdx + 1, elemToken2); 327 } 328 } 329 generator.writeEndArray(); 330 } else { 331 if (currentToken != JsonToken.START_ARRAY) { 332 skipValue(parser, currentToken); 333 generator.writeNull(); 334 return; 335 } 336 int currentIdx = 0; 337 boolean found = false; 338 JsonToken arrToken2; 339 while ((arrToken2 = parser.nextToken()) != null && arrToken2 != JsonToken.END_ARRAY) { 340 if (currentIdx == seg.index) { 341 extractPath(parser, generator, segments, segIdx + 1); 342 found = true; 343 JsonToken skipToken2; 344 while ((skipToken2 = parser.nextToken()) != null && skipToken2 != JsonToken.END_ARRAY) { 345 skipValue(parser, skipToken2); 346 } 347 break; 348 } else { 349 skipValue(parser, arrToken2); 350 } 351 currentIdx++; 352 } 353 if (!found) { 354 generator.writeNull(); 355 } 356 } 357 } 358 359 // ------------------------------------------------------------------------- 360 // Copy / skip helpers 361 // ------------------------------------------------------------------------- 362 363 /** 364 * Copy the next complete value from {@code parser} to {@code generator}. Advances the parser by 365 * one token internally. 366 */ 367 private void copyValue(JsonParser parser, JsonGenerator generator) throws IOException { 368 JsonToken token = parser.nextToken(); 369 if (token == null) { 370 generator.writeNull(); 371 return; 372 } 373 copyValue(parser, generator, token); 374 } 375 376 /** 377 * Copy a complete value starting at {@code token} (already read) from {@code parser} to {@code 378 * generator}. 379 */ 380 private void copyValue(JsonParser parser, JsonGenerator generator, JsonToken token) 381 throws IOException { 382 switch (token) { 383 case START_OBJECT: 384 generator.writeStartObject(); 385 JsonToken objToken; 386 while ((objToken = parser.nextToken()) != null && objToken != JsonToken.END_OBJECT) { 387 generator.writeFieldName(parser.currentName()); 388 copyValue(parser, generator); 389 } 390 generator.writeEndObject(); 391 break; 392 393 case START_ARRAY: 394 generator.writeStartArray(); 395 while (true) { 396 JsonToken t = parser.nextToken(); 397 if (t == JsonToken.END_ARRAY) break; 398 copyValue(parser, generator, t); 399 } 400 generator.writeEndArray(); 401 break; 402 403 case VALUE_STRING: 404 generator.writeString(parser.getText()); 405 break; 406 407 case VALUE_NUMBER_INT: 408 generator.writeNumber(parser.getLongValue()); 409 break; 410 411 case VALUE_NUMBER_FLOAT: 412 generator.writeNumber(parser.getDoubleValue()); 413 break; 414 415 case VALUE_TRUE: 416 generator.writeBoolean(true); 417 break; 418 419 case VALUE_FALSE: 420 generator.writeBoolean(false); 421 break; 422 423 default: 424 generator.writeNull(); 425 break; 426 } 427 } 428 429 /** 430 * Skip over a complete value starting at {@code token} (already read). Does not write anything. 431 */ 432 private void skipValue(JsonParser parser, JsonToken token) throws IOException { 433 if (token == null) { 434 return; 435 } 436 switch (token) { 437 case START_OBJECT: 438 int objDepth = 1; 439 while (objDepth > 0) { 440 JsonToken t = parser.nextToken(); 441 if (t == JsonToken.START_OBJECT) objDepth++; 442 else if (t == JsonToken.END_OBJECT) objDepth--; 443 } 444 break; 445 446 case START_ARRAY: 447 int arrDepth = 1; 448 while (arrDepth > 0) { 449 JsonToken t = parser.nextToken(); 450 if (t == JsonToken.START_ARRAY) arrDepth++; 451 else if (t == JsonToken.END_ARRAY) arrDepth--; 452 } 453 break; 454 455 default: 456 // Scalar values are self-contained – nothing more to skip 457 break; 458 } 459 } 460}