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}