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}