001package com.streamconverter.command.impl.json; 002 003import com.fasterxml.jackson.databind.JsonNode; 004import com.fasterxml.jackson.databind.ObjectMapper; 005import com.streamconverter.command.AbstractStreamCommand; 006import com.streamconverter.path.IPath; 007import java.io.BufferedReader; 008import java.io.IOException; 009import java.io.InputStream; 010import java.io.InputStreamReader; 011import java.io.OutputStream; 012import java.io.OutputStreamWriter; 013import java.io.Writer; 014import java.nio.charset.StandardCharsets; 015import java.util.Arrays; 016import java.util.List; 017 018/** 019 * JSON Filter Command Class 020 * 021 * <p>This class implements pure data extraction from JSON using TreePath expressions. Unlike 022 * JsonNavigateCommand which applies transformations, JsonFilterCommand only extracts/filters data 023 * based on specified paths without any modifications. 024 * 025 * <p>Features: - Extract specific elements using TreePath expressions - Preserve exact data types 026 * and structure of extracted elements - Memory-efficient processing for large JSON files - Support 027 * for simple path expressions 028 */ 029public class JsonFilterCommand extends AbstractStreamCommand { 030 031 private static final int BUFFER_SIZE = 8192; // 8KB buffer for streaming 032 private static final int MAX_MEMORY_BUFFER = 10 * 1024 * 1024; // 10MB max buffer 033 034 private final IPath<List<String>> jsonPath; 035 private final ObjectMapper objectMapper; 036 037 /** 038 * Constructor for JSON filtering with typed TreePath selector. 039 * 040 * @param jsonPath the typed TreePath to extract data 041 * @throws IllegalArgumentException if jsonPath is null 042 */ 043 public JsonFilterCommand(IPath<List<String>> jsonPath) { 044 if (jsonPath == null) { 045 throw new IllegalArgumentException("TreePath cannot be null"); 046 } 047 this.jsonPath = jsonPath; 048 jsonPath.toString(); 049 this.objectMapper = new ObjectMapper(); 050 } 051 052 @Override 053 protected String getCommandDetails() { 054 return String.format("JsonFilterCommand(jsonPath='%s')", jsonPath.toString()); 055 } 056 057 @Override 058 protected void executeInternal(InputStream inputStream, OutputStream outputStream) 059 throws IOException { 060 try (BufferedReader reader = 061 new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); 062 Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { 063 064 // Read JSON content efficiently 065 String jsonContent = readJsonContent(reader); 066 067 if (jsonContent.trim().isEmpty()) { 068 writer.write("null"); 069 writer.flush(); 070 return; 071 } 072 073 try { 074 // Apply simple TreePath-like extraction using lightweight parsing 075 String result = extractJsonValue(jsonContent, jsonPath.toString()); 076 writer.write(result); 077 writer.flush(); 078 079 } catch (Exception e) { 080 // If extraction fails, return null 081 writer.write("null"); 082 writer.flush(); 083 } 084 } 085 } 086 087 /** 088 * Read JSON content from reader with memory management 089 * 090 * @param reader the BufferedReader to read from 091 * @return JSON content as string, or empty string if no content 092 * @throws IOException if reading fails 093 */ 094 private String readJsonContent(BufferedReader reader) throws IOException { 095 StringBuilder jsonBuilder = new StringBuilder(); 096 char[] buffer = new char[BUFFER_SIZE]; 097 int totalCharsRead = 0; 098 int charsRead; 099 100 while ((charsRead = reader.read(buffer)) != -1) { 101 totalCharsRead += charsRead; 102 103 // Memory protection: prevent reading excessively large JSON into memory 104 if (totalCharsRead > MAX_MEMORY_BUFFER) { 105 throw new IOException( 106 "JSON content too large for filtering. Use streaming NavigateCommand instead."); 107 } 108 109 jsonBuilder.append(buffer, 0, charsRead); 110 } 111 112 return jsonBuilder.toString(); // Return empty string instead of null 113 } 114 115 /** 116 * Extract JSON value using simple TreePath-like expressions 117 * 118 * @param jsonContent the JSON content string 119 * @param path the TreePath expression (simplified) 120 * @return extracted value as JSON string 121 */ 122 private String extractJsonValue(String jsonContent, String path) { 123 try { 124 JsonNode rootNode = objectMapper.readTree(jsonContent); 125 126 // Handle root path 127 if ("$".equals(path)) { 128 return objectMapper.writeValueAsString(rootNode); 129 } 130 131 // Handle special case of $[*].property (root array with wildcard) 132 if (path.startsWith("$[*].") && path.length() > 5) { 133 String propertyPath = path.substring(5); // Remove "$[*]." 134 if (rootNode.isArray()) { 135 StringBuilder resultBuilder = new StringBuilder("["); 136 boolean first = true; 137 for (JsonNode arrayElement : rootNode) { 138 if (!first) resultBuilder.append(","); 139 first = false; 140 141 // Apply the property path to each array element 142 String[] propertySegments = propertyPath.split("\\."); 143 JsonNode extractedNode = arrayElement; 144 for (String segment : propertySegments) { 145 if (segment.isEmpty()) continue; 146 extractedNode = extractedNode.get(segment); 147 if (extractedNode == null) { 148 extractedNode = objectMapper.getNodeFactory().nullNode(); 149 break; 150 } 151 } 152 resultBuilder.append(objectMapper.writeValueAsString(extractedNode)); 153 } 154 resultBuilder.append("]"); 155 return resultBuilder.toString(); 156 } else { 157 return "null"; 158 } 159 } 160 161 // Remove the '$.' prefix if present 162 String normalizedPath = path.startsWith("$.") ? path.substring(2) : path; 163 164 // Navigate through the path 165 JsonNode currentNode = rootNode; 166 String[] pathSegments = normalizedPath.split("\\."); 167 168 for (String segment : pathSegments) { 169 if (segment.isEmpty()) continue; 170 171 // Handle array indexing (e.g., "users[0]") 172 if (segment.contains("[") && segment.endsWith("]")) { 173 String fieldName = segment.substring(0, segment.indexOf("[")); 174 String indexStr = segment.substring(segment.indexOf("[") + 1, segment.indexOf("]")); 175 176 if (!fieldName.isEmpty()) { 177 currentNode = currentNode.get(fieldName); 178 if (currentNode == null) return "null"; 179 } 180 181 // Handle wildcard array access [*] 182 if ("*".equals(indexStr)) { 183 if (currentNode.isArray()) { 184 // For wildcard, we need to handle subsequent path segments differently 185 // This is a simplified implementation that extracts all matching elements 186 StringBuilder resultBuilder = new StringBuilder("["); 187 boolean first = true; 188 for (JsonNode arrayElement : currentNode) { 189 if (!first) resultBuilder.append(","); 190 first = false; 191 192 // If there are more path segments, apply them to each array element 193 String remainingPath = 194 String.join( 195 ".", 196 Arrays.copyOfRange( 197 pathSegments, 198 Arrays.asList(pathSegments).indexOf(segment) + 1, 199 pathSegments.length)); 200 if (!remainingPath.isEmpty()) { 201 JsonNode extractedNode = arrayElement; 202 String[] remainingSegments = remainingPath.split("\\."); 203 for (String remainingSeg : remainingSegments) { 204 if (remainingSeg.isEmpty()) continue; 205 extractedNode = extractedNode.get(remainingSeg); 206 if (extractedNode == null) { 207 extractedNode = objectMapper.getNodeFactory().nullNode(); 208 break; 209 } 210 } 211 resultBuilder.append(objectMapper.writeValueAsString(extractedNode)); 212 } else { 213 resultBuilder.append(objectMapper.writeValueAsString(arrayElement)); 214 } 215 } 216 resultBuilder.append("]"); 217 return resultBuilder.toString(); 218 } else { 219 return "null"; 220 } 221 } else { 222 // Regular array indexing 223 try { 224 int index = Integer.parseInt(indexStr); 225 if (currentNode.isArray() && index >= 0 && index < currentNode.size()) { 226 currentNode = currentNode.get(index); 227 } else { 228 return "null"; 229 } 230 } catch (NumberFormatException e) { 231 return "null"; 232 } 233 } 234 } else { 235 // Simple property access 236 currentNode = currentNode.get(segment); 237 if (currentNode == null) { 238 return "null"; 239 } 240 } 241 } 242 243 return objectMapper.writeValueAsString(currentNode); 244 } catch (Exception e) { 245 // If JSON parsing fails, fall back to simple string-based extraction 246 return extractSimplePropertyFallback(jsonContent, path); 247 } 248 } 249 250 /** 251 * Fallback method for simple string-based extraction when JSON parsing fails 252 * 253 * @param jsonContent the JSON content string 254 * @param path the TreePath expression 255 * @return extracted value as JSON string or original content 256 */ 257 private String extractSimplePropertyFallback(String jsonContent, String path) { 258 // Handle root path 259 if ("$".equals(path)) { 260 return jsonContent.trim(); 261 } 262 263 // Simple property extraction: $.property 264 if (path.startsWith("$.") && !path.contains("[") && path.indexOf(".", 2) == -1) { 265 String property = path.substring(2); 266 return extractSimpleProperty(jsonContent, property); 267 } 268 269 // For other complex paths that couldn't be parsed, return null instead of original content 270 return "null"; 271 } 272 273 /** 274 * Extract a simple property from JSON content 275 * 276 * @param jsonContent JSON string 277 * @param property property name to extract 278 * @return property value as JSON string or "null" if not found 279 */ 280 private String extractSimpleProperty(String jsonContent, String property) { 281 String searchPattern = "\"" + property + "\":"; 282 int propertyStart = jsonContent.indexOf(searchPattern); 283 284 if (propertyStart == -1) { 285 return "null"; // Property not found 286 } 287 288 // Find the start of the value 289 int colonIndex = propertyStart + searchPattern.length(); 290 int valueStart = colonIndex; 291 while (valueStart < jsonContent.length() 292 && Character.isWhitespace(jsonContent.charAt(valueStart))) { 293 valueStart++; 294 } 295 296 if (valueStart >= jsonContent.length()) { 297 return "null"; 298 } 299 300 // Extract the value based on its type 301 char firstChar = jsonContent.charAt(valueStart); 302 303 if (firstChar == '"') { 304 // String value 305 return extractQuotedString(jsonContent, valueStart); 306 } else if (firstChar == '{') { 307 // Object value 308 return extractJsonObject(jsonContent, valueStart); 309 } else if (firstChar == '[') { 310 // Array value 311 return extractJsonArray(jsonContent, valueStart); 312 } else { 313 // Number, boolean, or null 314 return extractSimpleValue(jsonContent, valueStart); 315 } 316 } 317 318 private String extractQuotedString(String json, int start) { 319 StringBuilder result = new StringBuilder(); 320 result.append('"'); 321 int i = start + 1; // Skip opening quote 322 323 while (i < json.length()) { 324 char c = json.charAt(i); 325 if (c == '"' && (i == start + 1 || json.charAt(i - 1) != '\\')) { 326 result.append('"'); 327 break; 328 } 329 result.append(c); 330 i++; 331 } 332 333 return result.toString(); 334 } 335 336 private String extractJsonObject(String json, int start) { 337 StringBuilder result = new StringBuilder(); 338 int braceCount = 0; 339 340 for (int i = start; i < json.length(); i++) { 341 char c = json.charAt(i); 342 result.append(c); 343 344 if (c == '{') braceCount++; 345 else if (c == '}') braceCount--; 346 347 if (braceCount == 0) break; 348 } 349 350 return result.toString(); 351 } 352 353 private String extractJsonArray(String json, int start) { 354 StringBuilder result = new StringBuilder(); 355 int bracketCount = 0; 356 357 for (int i = start; i < json.length(); i++) { 358 char c = json.charAt(i); 359 result.append(c); 360 361 if (c == '[') bracketCount++; 362 else if (c == ']') bracketCount--; 363 364 if (bracketCount == 0) break; 365 } 366 367 return result.toString(); 368 } 369 370 private String extractSimpleValue(String json, int start) { 371 StringBuilder result = new StringBuilder(); 372 373 for (int i = start; i < json.length(); i++) { 374 char c = json.charAt(i); 375 if (c == ',' || c == '}' || c == ']' || Character.isWhitespace(c)) { 376 break; 377 } 378 result.append(c); 379 } 380 381 return result.toString(); 382 } 383}