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.command.rule.IRule;
009import com.streamconverter.path.TreePath;
010import java.io.IOException;
011import java.io.InputStream;
012import java.io.OutputStream;
013import java.util.ArrayList;
014import java.util.List;
015
016/**
017 * JSON Navigation Command for applying transformations to JSON data
018 *
019 * <p>This command navigates through JSON structures and applies transformations using rules while
020 * preserving the overall JSON structure. It focuses purely on navigation and transformation, not
021 * extraction.
022 *
023 * <p>Responsibilities: - Navigate to specified JSON paths - Apply transformation rules to matching
024 * elements - Preserve JSON structure during transformation - Stream processing for memory
025 * efficiency
026 */
027public class JsonNavigateCommand extends AbstractStreamCommand {
028
029  private final TreePath treePath;
030  private final IRule rule;
031  private final JsonFactory jsonFactory;
032
033  /**
034   * Constructor for JSON navigation with TreePath selector and transformation rule.
035   *
036   * @param treePath the TreePath to select data
037   * @param rule the transformation rule to apply to selected elements
038   * @throws IllegalArgumentException if treePath or rule is null
039   */
040  private JsonNavigateCommand(TreePath treePath, IRule rule) {
041    this.treePath = treePath;
042    this.rule = rule;
043    this.jsonFactory = new JsonFactory();
044  }
045
046  /**
047   * Factory method for creating a JSON navigation command with TreePath and rule.
048   *
049   * @param treePath the TreePath to select data
050   * @param rule the transformation rule to apply to selected elements
051   * @return a JsonNavigateCommand that transforms the specified path with the given rule
052   * @throws IllegalArgumentException if treePath or rule is null
053   */
054  public static JsonNavigateCommand create(TreePath treePath, IRule rule) {
055    if (treePath == null) {
056      throw new IllegalArgumentException("TreePath cannot be null");
057    }
058    if (rule == null) {
059      throw new IllegalArgumentException("Rule cannot be null");
060    }
061    return new JsonNavigateCommand(treePath, rule);
062  }
063
064  @Override
065  public void execute(InputStream inputStream, OutputStream outputStream) throws IOException {
066    processJsonWithStreaming(inputStream, outputStream);
067  }
068
069  /** Stream JSON processing with structure preservation */
070  private void processJsonWithStreaming(InputStream inputStream, OutputStream outputStream)
071      throws IOException {
072    try (JsonParser parser = jsonFactory.createParser(inputStream);
073        JsonGenerator generator = jsonFactory.createGenerator(outputStream)) {
074
075      // Process with JSONPath filtering - transform matching elements
076      processJsonStreamWithPath(parser, generator);
077
078      generator.flush();
079    }
080  }
081
082  /** Process JSON with specific JSONPath targeting */
083  private void processJsonStreamWithPath(JsonParser parser, JsonGenerator generator)
084      throws IOException {
085    // depth tracks how many objects deep we are (0 = before root object)
086    int depth = 0;
087    // currentPath[i] holds the field name active at object-depth i+1
088    List<String> currentPath = new ArrayList<>();
089    JsonToken token;
090
091    while ((token = parser.nextToken()) != null) {
092      switch (token) {
093        case FIELD_NAME:
094          String fieldName = parser.currentName();
095          // Ensure currentPath has a slot for depth (1-based object depth)
096          while (currentPath.size() < depth) {
097            currentPath.add(null);
098          }
099          while (currentPath.size() > depth) {
100            currentPath.remove(currentPath.size() - 1);
101          }
102          if (depth > 0) {
103            currentPath.set(depth - 1, fieldName);
104          }
105          generator.writeFieldName(fieldName);
106          break;
107
108        case VALUE_STRING:
109          String originalValue = parser.getText();
110          if (isMatchingPath(currentPath)) {
111            String transformed;
112            try {
113              transformed = rule.apply(originalValue);
114            } catch (RuntimeException ruleEx) {
115              throw new IOException("Rule application failed at path " + currentPath, ruleEx);
116            }
117            generator.writeString(transformed);
118          } else {
119            generator.writeString(originalValue);
120          }
121          break;
122
123        case START_OBJECT:
124          depth++;
125          generator.writeStartObject();
126          break;
127
128        case END_OBJECT:
129          depth--;
130          // Trim path to current depth
131          while (currentPath.size() > depth) {
132            currentPath.remove(currentPath.size() - 1);
133          }
134          generator.writeEndObject();
135          break;
136
137        case START_ARRAY:
138          generator.writeStartArray();
139          break;
140
141        case END_ARRAY:
142          generator.writeEndArray();
143          break;
144
145        default:
146          // Copy all other tokens as-is (numbers, booleans, null)
147          generator.copyCurrentEvent(parser);
148          break;
149      }
150    }
151  }
152
153  /** Simple path matching for streaming JSON processing */
154  private boolean isMatchingPath(List<String> currentPath) {
155    // Use matchesIgnoringArraySyntax so that paths like $.orders[*].product_code
156    // correctly match the streaming currentPath ["orders", "product_code"].
157    return treePath.matchesIgnoringArraySyntax(currentPath);
158  }
159}