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.fasterxml.jackson.databind.ObjectMapper;
008import com.streamconverter.command.AbstractStreamCommand;
009import com.streamconverter.command.rule.IRule;
010import com.streamconverter.path.TreePath;
011import java.io.IOException;
012import java.io.InputStream;
013import java.io.OutputStream;
014import java.nio.charset.StandardCharsets;
015import java.util.ArrayList;
016import java.util.List;
017
018/**
019 * JSON Navigation Command for applying transformations to JSON data
020 *
021 * <p>This command navigates through JSON structures and applies transformations using rules while
022 * preserving the overall JSON structure. It focuses purely on navigation and transformation, not
023 * extraction.
024 *
025 * <p>Responsibilities: - Navigate to specified JSON paths - Apply transformation rules to matching
026 * elements - Preserve JSON structure during transformation - Stream processing for memory
027 * efficiency
028 */
029public class JsonNavigateCommand extends AbstractStreamCommand {
030
031  private final TreePath treePath;
032  private final IRule rule;
033  private final ObjectMapper objectMapper;
034
035  /**
036   * Constructor for JSON navigation with TreePath selector and transformation rule.
037   *
038   * @param treePath the TreePath to select data
039   * @param rule the transformation rule to apply to selected elements
040   * @throws IllegalArgumentException if treePath or rule is null
041   */
042  public JsonNavigateCommand(TreePath treePath, IRule rule) {
043    if (treePath == null) {
044      throw new IllegalArgumentException("TreePath cannot be null");
045    }
046    if (rule == null) {
047      throw new IllegalArgumentException("Rule cannot be null");
048    }
049    this.treePath = treePath;
050    this.rule = rule;
051    this.objectMapper = new ObjectMapper();
052  }
053
054  /**
055   * Factory method for creating a JSON navigation command with TreePath and rule.
056   *
057   * @param treePath the TreePath to select data
058   * @param rule the transformation rule to apply to selected elements
059   * @return a JsonNavigateCommand that transforms the specified path with the given rule
060   * @throws IllegalArgumentException if rule is null
061   */
062  public static JsonNavigateCommand create(TreePath treePath, IRule rule) {
063    return new JsonNavigateCommand(treePath, rule);
064  }
065
066  @Override
067  protected String getCommandDetails() {
068    return String.format(
069        "JsonNavigateCommand(treePath='%s', rule='%s')",
070        treePath.toString(), rule.getClass().getSimpleName());
071  }
072
073  @Override
074  protected void executeInternal(InputStream inputStream, OutputStream outputStream)
075      throws IOException {
076    processJsonWithStreaming(inputStream, outputStream);
077  }
078
079  /** Stream JSON processing with structure preservation */
080  private void processJsonWithStreaming(InputStream inputStream, OutputStream outputStream)
081      throws IOException {
082    JsonFactory jsonFactory = objectMapper.getFactory();
083
084    try (JsonParser parser = jsonFactory.createParser(inputStream);
085        JsonGenerator generator = jsonFactory.createGenerator(outputStream)) {
086
087      // Process with JSONPath filtering - transform matching elements
088      processJsonStreamWithPath(parser, generator);
089
090      generator.flush();
091    } catch (com.fasterxml.jackson.core.JsonParseException e) {
092      handleJsonParseException(outputStream, e);
093    }
094  }
095
096  /** Process JSON with specific JSONPath targeting */
097  private void processJsonStreamWithPath(JsonParser parser, JsonGenerator generator)
098      throws IOException {
099    List<String> currentPath = new ArrayList<>();
100    JsonToken token;
101
102    while ((token = parser.nextToken()) != null) {
103      switch (token) {
104        case FIELD_NAME:
105          String fieldName = parser.currentName();
106
107          // Reset path for new field at current level
108          if (!currentPath.isEmpty() && currentPath.size() > 1) {
109            // Remove previous sibling field from path
110            currentPath.set(currentPath.size() - 1, fieldName);
111          } else {
112            // Clear and add current field
113            currentPath.clear();
114            currentPath.add(fieldName);
115          }
116
117          generator.writeFieldName(fieldName);
118          break;
119
120        case VALUE_STRING:
121          String originalValue = parser.getText();
122          boolean inTargetPath = isMatchingPath(currentPath);
123          if (inTargetPath) {
124            String transformedValue = rule.apply(originalValue);
125            generator.writeString(transformedValue);
126          } else {
127            generator.writeString(originalValue);
128          }
129          break;
130
131        case START_OBJECT:
132          generator.writeStartObject();
133          // Don't modify path on object start
134          break;
135
136        case END_OBJECT:
137          generator.writeEndObject();
138          // Remove one level from path
139          if (currentPath.size() > 1) {
140            currentPath.remove(currentPath.size() - 1);
141          }
142          break;
143
144        case START_ARRAY:
145          generator.writeStartArray();
146          break;
147
148        case END_ARRAY:
149          generator.writeEndArray();
150          break;
151
152        default:
153          // Copy all other tokens as-is (numbers, booleans, null)
154          generator.copyCurrentEvent(parser);
155          break;
156      }
157    }
158  }
159
160  /** Simple path matching for streaming JSON processing */
161  private boolean isMatchingPath(List<String> currentPath) {
162    return treePath.matches(currentPath);
163  }
164
165  /** Handle JSON parsing exceptions */
166  private void handleJsonParseException(OutputStream outputStream, Exception e) throws IOException {
167    String errorMessage = String.format("JSON parsing error: %s", e.getMessage());
168    outputStream.write(errorMessage.getBytes(StandardCharsets.UTF_8));
169    outputStream.flush();
170  }
171}