001package com.streamconverter.command.impl.csv; 002 003import com.streamconverter.command.AbstractStreamCommand; 004import com.streamconverter.command.rule.IRule; 005import com.streamconverter.path.CSVPath; 006import com.streamconverter.path.TreePath; 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; 015 016/** 017 * CSV Navigate Command Class 018 * 019 * <p>This class implements command for targeted CSV transformation using column selectors. It 020 * identifies specific columns using column names or indices and applies IRule transformations to 021 * those columns while preserving the overall CSV structure. 022 */ 023public class CsvNavigateCommand extends AbstractStreamCommand { 024 025 private final CSVPath columnSelector; 026 private final IRule rule; 027 private int columnIndex = -1; 028 029 /** 030 * Constructor for CSV navigation with column selector and transformation rule. 031 * 032 * @param columnSelector the column name or index to select (e.g., "name", "2") 033 * @param rule the transformation rule to apply to selected column 034 * @throws IllegalArgumentException if columnSelector or rule is null 035 * @deprecated Use {@link #CsvNavigateCommand(CSVPath, IRule)} instead 036 */ 037 @Deprecated 038 public CsvNavigateCommand(String columnSelector, IRule rule) { 039 if (columnSelector == null) { 040 throw new IllegalArgumentException("Column selector cannot be null"); 041 } 042 if (rule == null) { 043 throw new IllegalArgumentException("Rule cannot be null"); 044 } 045 this.columnSelector = new CSVPath(columnSelector); 046 this.rule = rule; 047 } 048 049 /** 050 * Constructor for CSV navigation with typed column selector and transformation rule. 051 * 052 * @param columnSelector the typed CSVPath to select column 053 * @param rule the transformation rule to apply to selected column 054 * @throws IllegalArgumentException if columnSelector or rule is null 055 */ 056 public CsvNavigateCommand(CSVPath columnSelector, IRule rule) { 057 if (columnSelector == null) { 058 throw new IllegalArgumentException("Column selector cannot be null"); 059 } 060 if (rule == null) { 061 throw new IllegalArgumentException("Rule cannot be null"); 062 } 063 this.columnSelector = columnSelector; 064 columnSelector.toString(); 065 this.rule = rule; 066 } 067 068 /** 069 * Constructor for CSV navigation with TreePath (for compatibility). 070 * 071 * @param treePath the TreePath representing column selector 072 * @param rule the transformation rule to apply to selected column 073 * @throws IllegalArgumentException if treePath or rule is null 074 */ 075 public CsvNavigateCommand(TreePath treePath, IRule rule) { 076 if (treePath == null) { 077 throw new IllegalArgumentException("TreePath cannot be null"); 078 } 079 if (rule == null) { 080 throw new IllegalArgumentException("Rule cannot be null"); 081 } 082 // Convert TreePath to CSVPath - assume simple column name 083 String columnName = treePath.toString(); 084 this.columnSelector = new CSVPath(columnName); 085 this.rule = rule; 086 } 087 088 /** 089 * Factory method for creating a CSV navigation command with explicit rule specification. This 090 * method makes the intention explicit: extract data from the specified column and apply the given 091 * transformation rule. 092 * 093 * @param columnSelector the column name or index to select (e.g., "name", "2") 094 * @param rule the transformation rule to apply to selected column data 095 * @return a CsvNavigateCommand that extracts the specified column with the given rule 096 * @throws IllegalArgumentException if rule is null 097 * @deprecated Use {@link #create(CSVPath, IRule)} instead 098 */ 099 @Deprecated 100 public static CsvNavigateCommand create(String columnSelector, IRule rule) { 101 return new CsvNavigateCommand(columnSelector, rule); 102 } 103 104 /** 105 * Factory method for creating a CSV navigation command with typed column selector and rule. 106 * 107 * @param columnSelector the typed CSVPath to select column 108 * @param rule the transformation rule to apply to selected column data 109 * @return a CsvNavigateCommand that extracts the specified column with the given rule 110 * @throws IllegalArgumentException if rule is null 111 */ 112 public static CsvNavigateCommand create(CSVPath columnSelector, IRule rule) { 113 return new CsvNavigateCommand(columnSelector, rule); 114 } 115 116 @Override 117 protected String getCommandDetails() { 118 return String.format( 119 "CsvNavigateCommand(columnSelector='%s', rule='%s')", 120 columnSelector.toString(), rule.getClass().getSimpleName()); 121 } 122 123 @Override 124 protected void executeInternal(InputStream inputStream, OutputStream outputStream) 125 throws IOException { 126 try (BufferedReader reader = 127 new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); 128 Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { 129 130 // Apply rule to specific column while preserving structure 131 applyRuleToColumn(reader, writer); 132 } 133 } 134 135 /** Apply transformation rule to specific column while preserving CSV structure */ 136 private void applyRuleToColumn(BufferedReader reader, Writer writer) throws IOException { 137 String headerLine = reader.readLine(); 138 if (headerLine == null) { 139 return; // Empty input 140 } 141 142 String[] headers = parseCSVLine(headerLine); 143 144 // Determine column index if selector is provided 145 columnIndex = resolveColumnIndex(headers, columnSelector); 146 if (columnIndex == -1) { 147 throw new IllegalArgumentException("Column not found: " + columnSelector.toString()); 148 } 149 150 // Write header (unchanged) 151 writer.write(headerLine); 152 writer.write(System.lineSeparator()); 153 154 // Process data rows 155 String line; 156 while ((line = reader.readLine()) != null) { 157 String[] values = parseCSVLine(line); 158 159 if (columnIndex < values.length) { 160 // Apply rule to target column only 161 values[columnIndex] = rule.apply(values[columnIndex]); 162 } 163 164 // Write entire row with transformed column (with proper CSV escaping) 165 writer.write(formatCsvRow(values)); 166 writer.write(System.lineSeparator()); 167 } 168 writer.flush(); 169 } 170 171 private String[] parseCSVLine(String line) { 172 return line.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)"); 173 } 174 175 /** Format CSV row with proper escaping */ 176 private String formatCsvRow(String[] values) { 177 StringBuilder row = new StringBuilder(); 178 for (int i = 0; i < values.length; i++) { 179 if (i > 0) { 180 row.append(","); 181 } 182 row.append(escapeCsvValue(values[i])); 183 } 184 return row.toString(); 185 } 186 187 /** Escape CSV value according to CSV standards */ 188 private String escapeCsvValue(String value) { 189 if (value == null) { 190 return ""; 191 } 192 193 // Check if value needs escaping (contains comma, quote, or newline) 194 if (value.contains(",") 195 || value.contains("\"") 196 || value.contains("\n") 197 || value.contains("\r")) { 198 // Escape quotes by doubling them and wrap entire value in quotes 199 return "\"" + value.replace("\"", "\"\"") + "\""; 200 } 201 202 return value; 203 } 204 205 /** Resolve column index using CSVPath matches() method */ 206 private int resolveColumnIndex(String[] headers, CSVPath csvPath) { 207 // Use matches() method to check each column 208 for (int i = 0; i < headers.length; i++) { 209 if (csvPath.matches(headers, i)) { 210 return i; 211 } 212 } 213 return -1; // Not found 214 } 215}