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}