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}