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}