001package com.streamconverter.examples;
002
003import com.streamconverter.StreamConverter;
004import com.streamconverter.StreamProcessingException;
005import com.streamconverter.command.impl.FileBufferCommand;
006import com.streamconverter.command.impl.csv.CsvNavigateCommand;
007import com.streamconverter.command.impl.csv.CsvValidateCommand;
008import com.streamconverter.command.rule.impl.string.TrimRule;
009import com.streamconverter.path.CSVPath;
010import java.io.ByteArrayInputStream;
011import java.io.ByteArrayOutputStream;
012import java.io.IOException;
013import java.nio.charset.StandardCharsets;
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017/**
018 * 例5: ConsumerCommand(検証) × FileBufferCommand による安全なパイプライン
019 *
020 * <p>並列パイプラインにおける検証コマンドの落とし穴と、その解決策を示す。
021 *
022 * <p><b>この例で学べること:</b>
023 *
024 * <ul>
025 *   <li>{@link CsvValidateCommand} は {@link com.streamconverter.command.ConsumerCommand} のサブクラスで、
026 *       入力を読みながら出力にも同時に流す(TeeInputStream)
027 *   <li>各コマンドは並列実行されるため、検証が失敗する前に後段へデータが流れ始める
028 *   <li>{@link FileBufferCommand} を挿入すると前段の完全完了を待ってから後段が開始する(逐次化)
029 *   <li>{@link FileBufferCommand#createEncrypted()} で一時ファイルを AES-256-GCM 暗号化できる
030 * </ul>
031 *
032 * <p><b>3つのパターンを比較する:</b>
033 *
034 * <pre>
035 * パターンA(問題あり):
036 *   CsvValidateCommand → CsvNavigateCommand
037 *   検証失敗でも後段に部分データが届く可能性がある
038 *
039 * パターンB(安全):
040 *   CsvValidateCommand → FileBufferCommand → CsvNavigateCommand
041 *   FileBufferCommand が前段の完了を待ち、後段は確実に検証済みデータのみを受け取る
042 *
043 * パターンC(機密データ):
044 *   CsvValidateCommand → FileBufferCommand.createEncrypted() → CsvNavigateCommand
045 *   一時ファイルを AES-256-GCM で暗号化するため、機密データでも安全
046 * </pre>
047 */
048public class ValidationPipelineExample {
049
050  private static final Logger log = LoggerFactory.getLogger(ValidationPipelineExample.class);
051
052  /**
053   * @param args コマンドライン引数(未使用)
054   * @throws IOException I/O エラー
055   */
056  public static void main(String[] args) throws IOException {
057    log.info("=== 例5: ConsumerCommand × FileBufferCommand による安全なパイプライン ===");
058
059    String validCsv =
060        "id,name,email\n" + "1,  Alice  ,alice@example.com\n" + "2,  Bob  ,bob@example.com\n";
061
062    String invalidCsv =
063        "id,name\n" // email カラムが欠けている
064            + "1,Alice\n"
065            + "2,Bob\n";
066
067    log.info("正常データ(入力):\n{}", validCsv);
068    log.info("不正データ(入力):\n{}", invalidCsv);
069
070    log.info("--- パターンA: FileBufferCommand なし(問題あり) ---");
071    patternA(validCsv, invalidCsv);
072
073    log.info("--- パターンB: FileBufferCommand あり(安全) ---");
074    patternB(validCsv, invalidCsv);
075
076    log.info("--- パターンC: FileBufferCommand.createEncrypted()(機密データ向け) ---");
077    patternC(validCsv);
078  }
079
080  // ---------------------------------------------------------------------------
081  // パターンA: FileBufferCommand なし
082  // ---------------------------------------------------------------------------
083
084  private static void patternA(String validCsv, String invalidCsv) throws IOException {
085    // CsvValidateCommand: ConsumerCommand のサブクラス
086    // TeeInputStream を使って入力を読みながら出力にも同時に流す。
087    // そのため検証が失敗した時点で後段にはすでに部分データが流れている可能性がある。
088    CsvValidateCommand validator = CsvValidateCommand.create("id", "name", "email");
089    CsvNavigateCommand trimName = CsvNavigateCommand.create(CSVPath.of("name"), new TrimRule());
090
091    StreamConverter converter = StreamConverter.create(validator, trimName);
092
093    String validExpected =
094        "id,name,email\r\n" + "1,Alice,alice@example.com\r\n" + "2,Bob,bob@example.com\r\n";
095    log.info("パターンA + 正常データ:\n期待値:\n{}", validExpected);
096    try {
097      ByteArrayOutputStream out1 = new ByteArrayOutputStream();
098      converter.run(new ByteArrayInputStream(validCsv.getBytes(StandardCharsets.UTF_8)), out1);
099      log.info("出力:\n{}", out1.toString(StandardCharsets.UTF_8));
100    } catch (StreamProcessingException e) {
101      log.error("失敗: {}", e.getMessage());
102    }
103
104    // 同じ StreamConverter を使いまわすことはできないため再生成
105    validator = CsvValidateCommand.create("id", "name", "email");
106    trimName = CsvNavigateCommand.create(CSVPath.of("name"), new TrimRule());
107    converter = StreamConverter.create(validator, trimName);
108
109    log.info(
110        "パターンA + 不正データ(email カラムなし):\n期待値: StreamProcessingException が発生するが、後段に部分データが届く可能性がある");
111    try {
112      ByteArrayOutputStream out2 = new ByteArrayOutputStream();
113      converter.run(new ByteArrayInputStream(invalidCsv.getBytes(StandardCharsets.UTF_8)), out2);
114      log.warn("成功してしまった(後段にデータが流れた可能性):\n{}", out2.toString(StandardCharsets.UTF_8));
115    } catch (StreamProcessingException e) {
116      log.info("出力(検証エラー、期待通り): {}", e.getMessage());
117    }
118  }
119
120  // ---------------------------------------------------------------------------
121  // パターンB: FileBufferCommand あり(逐次化)
122  // ---------------------------------------------------------------------------
123
124  private static void patternB(String validCsv, String invalidCsv) throws IOException {
125    // FileBufferCommand を検証コマンドの後に挿入することで逐次化できる。
126    // FileBufferCommand は:
127    //   1. 前段(CsvValidateCommand)の出力を一時ファイルに書き込む(前段が完全に完了するまで待機)
128    //   2. 前段完了後に一時ファイルを後段(CsvNavigateCommand)への入力として流す
129    // これにより「検証が成功した場合のみ後段が動く」ことが保証される。
130    String validExpected =
131        "id,name,email\r\n" + "1,Alice,alice@example.com\r\n" + "2,Bob,bob@example.com\r\n";
132    log.info("パターンB + 正常データ:\n期待値(パターンA と同一、FileBufferCommand は出力に影響しない):\n{}", validExpected);
133    try {
134      ByteArrayOutputStream out3 = new ByteArrayOutputStream();
135      StreamConverter.create(
136              CsvValidateCommand.create("id", "name", "email"),
137              FileBufferCommand.create(),
138              CsvNavigateCommand.create(CSVPath.of("name"), new TrimRule()))
139          .run(new ByteArrayInputStream(validCsv.getBytes(StandardCharsets.UTF_8)), out3);
140      log.info("出力:\n{}", out3.toString(StandardCharsets.UTF_8));
141    } catch (StreamProcessingException e) {
142      log.error("失敗: {}", e.getMessage());
143    }
144
145    log.info(
146        "パターンB + 不正データ(email カラムなし):\n期待値: StreamProcessingException が発生し、後段は一切実行されない(out4 は空)");
147    try {
148      ByteArrayOutputStream out4 = new ByteArrayOutputStream();
149      StreamConverter.create(
150              CsvValidateCommand.create("id", "name", "email"),
151              FileBufferCommand.create(),
152              CsvNavigateCommand.create(CSVPath.of("name"), new TrimRule()))
153          .run(new ByteArrayInputStream(invalidCsv.getBytes(StandardCharsets.UTF_8)), out4);
154      log.warn("後段が実行された(問題あり):\n{}", out4.toString(StandardCharsets.UTF_8));
155    } catch (StreamProcessingException e) {
156      log.info("出力(後段は実行されていない): {}", e.getMessage());
157    }
158  }
159
160  // ---------------------------------------------------------------------------
161  // パターンC: FileBufferCommand.createEncrypted()(機密データ向け)
162  // ---------------------------------------------------------------------------
163
164  private static void patternC(String validCsv) throws IOException {
165    // createEncrypted() は一時ファイルを AES-256-GCM で暗号化する。
166    // 鍵と IV は実行ごとに生成され、永続化されない。
167    // 機密情報を含む CSV を処理する場合に使用する。
168    String validExpected =
169        "id,name,email\r\n" + "1,Alice,alice@example.com\r\n" + "2,Bob,bob@example.com\r\n";
170    log.info("パターンC + 正常データ:\n期待値(パターンB と同一、暗号化は透過的):\n{}", validExpected);
171    try {
172      ByteArrayOutputStream out5 = new ByteArrayOutputStream();
173      StreamConverter.create(
174              CsvValidateCommand.create("id", "name", "email"),
175              FileBufferCommand.createEncrypted(),
176              CsvNavigateCommand.create(CSVPath.of("name"), new TrimRule()))
177          .run(new ByteArrayInputStream(validCsv.getBytes(StandardCharsets.UTF_8)), out5);
178      log.info("出力(一時ファイルは AES-256-GCM で暗号化):\n{}", out5.toString(StandardCharsets.UTF_8));
179    } catch (StreamProcessingException e) {
180      log.error("失敗: {}", e.getMessage());
181    }
182  }
183}