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}