001package com.streamconverter.command.impl.csv; 002 003import com.opencsv.CSVReader; 004import com.opencsv.CSVWriter; 005import com.opencsv.exceptions.CsvValidationException; 006import com.streamconverter.command.AbstractStreamCommand; 007import com.streamconverter.command.rule.IRule; 008import com.streamconverter.path.CSVPath; 009import com.streamconverter.path.TreePath; 010import java.io.IOException; 011import java.io.InputStream; 012import java.io.InputStreamReader; 013import java.io.OutputStream; 014import java.io.OutputStreamWriter; 015import java.nio.charset.StandardCharsets; 016 017/** 018 * CSV Navigate Command Class 019 * 020 * <p>This class implements command for targeted CSV transformation using column selectors. It 021 * identifies specific columns using column names or indices and applies IRule transformations to 022 * those columns while preserving the overall CSV structure. 023 */ 024public class CsvNavigateCommand extends AbstractStreamCommand { 025 026 private final CSVPath columnSelector; 027 private final IRule rule; 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 private CsvNavigateCommand(String columnSelector, IRule rule) { 039 this.columnSelector = CSVPath.of(columnSelector); 040 this.rule = rule; 041 } 042 043 /** 044 * Constructor for CSV navigation with typed column selector and transformation rule. 045 * 046 * @param columnSelector the typed CSVPath to select column 047 * @param rule the transformation rule to apply to selected column 048 * @throws IllegalArgumentException if columnSelector or rule is null 049 */ 050 private CsvNavigateCommand(CSVPath columnSelector, IRule rule) { 051 this.columnSelector = columnSelector; 052 this.rule = rule; 053 } 054 055 /** 056 * Constructor for CSV navigation with TreePath (for compatibility). 057 * 058 * @param treePath the TreePath representing column selector 059 * @param rule the transformation rule to apply to selected column 060 * @throws IllegalArgumentException if treePath or rule is null 061 */ 062 private CsvNavigateCommand(TreePath treePath, IRule rule) { 063 this.columnSelector = CSVPath.of(treePath.toString()); 064 this.rule = rule; 065 } 066 067 /** 068 * Factory method for creating a CSV navigation command with explicit rule specification. This 069 * method makes the intention explicit: extract data from the specified column and apply the given 070 * transformation rule. 071 * 072 * @param columnSelector the column name or index to select (e.g., "name", "2") 073 * @param rule the transformation rule to apply to selected column data 074 * @return a CsvNavigateCommand that extracts the specified column with the given rule 075 * @throws IllegalArgumentException if rule is null 076 * @deprecated Use {@link #create(CSVPath, IRule)} instead 077 */ 078 @Deprecated 079 public static CsvNavigateCommand create(String columnSelector, IRule rule) { 080 if (columnSelector == null) { 081 throw new IllegalArgumentException("Column selector cannot be null"); 082 } 083 if (rule == null) { 084 throw new IllegalArgumentException("Rule cannot be null"); 085 } 086 return new CsvNavigateCommand(columnSelector, rule); 087 } 088 089 /** 090 * Factory method for creating a CSV navigation command with typed column selector and rule. 091 * 092 * @param columnSelector the typed CSVPath to select column 093 * @param rule the transformation rule to apply to selected column data 094 * @return a CsvNavigateCommand that extracts the specified column with the given rule 095 * @throws IllegalArgumentException if columnSelector or rule is null 096 */ 097 public static CsvNavigateCommand create(CSVPath columnSelector, IRule rule) { 098 if (columnSelector == null) { 099 throw new IllegalArgumentException("Column selector cannot be null"); 100 } 101 if (rule == null) { 102 throw new IllegalArgumentException("Rule cannot be null"); 103 } 104 return new CsvNavigateCommand(columnSelector, rule); 105 } 106 107 /** 108 * Factory method for creating a CSV navigation command with TreePath compatibility. 109 * 110 * @param treePath the TreePath representing column selector 111 * @param rule the transformation rule to apply to selected column data 112 * @return a CsvNavigateCommand that extracts the specified column with the given rule 113 * @throws IllegalArgumentException if treePath or rule is null 114 */ 115 public static CsvNavigateCommand create(TreePath treePath, IRule rule) { 116 if (treePath == null) { 117 throw new IllegalArgumentException("TreePath cannot be null"); 118 } 119 if (rule == null) { 120 throw new IllegalArgumentException("Rule cannot be null"); 121 } 122 return new CsvNavigateCommand(treePath, rule); 123 } 124 125 @Override 126 public void execute(InputStream inputStream, OutputStream outputStream) throws IOException { 127 try (CSVReader csvReader = 128 new CSVReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); 129 CSVWriter csvWriter = 130 new CSVWriter( 131 new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), 132 CSVWriter.DEFAULT_SEPARATOR, 133 CSVWriter.DEFAULT_QUOTE_CHARACTER, 134 // RFC 4180 §5: only doubled-quote escaping, no backslash escape 135 CSVWriter.NO_ESCAPE_CHARACTER, 136 // RFC4180_LINE_END (\r\n) per RFC 4180 §2. 137 CSVWriter.RFC4180_LINE_END)) { 138 139 applyRuleToColumn(csvReader, csvWriter); 140 141 } catch (CsvValidationException e) { 142 throw new IOException("Failed to parse CSV: " + e.getMessage(), e); 143 } 144 } 145 146 /** Apply transformation rule to specific column while preserving CSV structure */ 147 private void applyRuleToColumn(CSVReader csvReader, CSVWriter csvWriter) 148 throws IOException, CsvValidationException { 149 String[] headers = csvReader.readNext(); 150 if (headers == null) { 151 return; // Empty input 152 } 153 154 // Determine column index 155 int columnIndex = resolveColumnIndex(headers, columnSelector); 156 if (columnIndex == -1) { 157 throw new IllegalArgumentException("Column not found: " + columnSelector.toString()); 158 } 159 160 // Write header (unchanged); applyQuotesToAll=false: only quote when RFC 4180 requires 161 csvWriter.writeNext(headers, false); 162 163 // Process data rows 164 String[] row; 165 while ((row = csvReader.readNext()) != null) { 166 if (columnIndex < row.length) { 167 // Apply rule to target column only 168 row[columnIndex] = rule.apply(row[columnIndex]); 169 } 170 // applyQuotesToAll=false: only quote fields that contain delimiters or quotes 171 csvWriter.writeNext(row, false); 172 } 173 csvWriter.flush(); 174 } 175 176 /** Resolve column index using CSVPath matches() method */ 177 private int resolveColumnIndex(String[] headers, CSVPath csvPath) { 178 for (int i = 0; i < headers.length; i++) { 179 if (csvPath.matches(headers, i)) { 180 return i; 181 } 182 } 183 return -1; // Not found 184 } 185}