001package com.streamconverter.examples; 002 003import com.streamconverter.StreamConverter; 004import com.streamconverter.command.impl.csv.CsvNavigateCommand; 005import com.streamconverter.command.impl.json.JsonNavigateCommand; 006import com.streamconverter.command.impl.xml.XmlNavigateCommand; 007import com.streamconverter.command.rule.IRule; 008import com.streamconverter.command.rule.impl.casing.CamelToSnakeCaseRule; 009import com.streamconverter.command.rule.impl.composite.ChainRule; 010import com.streamconverter.command.rule.impl.string.LowerCaseRule; 011import com.streamconverter.command.rule.impl.string.TrimRule; 012import com.streamconverter.path.CSVPath; 013import com.streamconverter.path.TreePath; 014import java.io.ByteArrayInputStream; 015import java.io.ByteArrayOutputStream; 016import java.io.IOException; 017import java.nio.charset.StandardCharsets; 018import org.slf4j.Logger; 019import org.slf4j.LoggerFactory; 020 021/** 022 * 例2: Navigate系コマンド × IRule(CSV/JSON/XML) 023 * 024 * <p>StreamConverter の「特定フィールドに変換ルールを適用する」パターンを示す。 025 * 026 * <p><b>この例で学べること:</b> 027 * 028 * <ul> 029 * <li>{@link CSVPath#of(String)} / {@link TreePath#fromJson(String)} / {@link 030 * TreePath#fromXml(String)} による要素指定の方法 031 * <li>{@link IRule} はラムダ式({@code s -> s.toUpperCase()})でも実装できる 032 * <li>{@link IRule} をクラスで実装することで複雑な変換ロジックを表現できる 033 * <li>組み込み Rule({@link TrimRule}, {@link LowerCaseRule}, {@link CamelToSnakeCaseRule})の使い方 034 * <li>{@link ChainRule} で複数の Rule を連鎖させる方法 035 * <li>CSV/JSON/XML のいずれも同じ Navigate + Rule のパターンで処理できること 036 * </ul> 037 * 038 * <p><b>シナリオ(CSV 3段パイプライン):</b> 039 * 040 * <pre> 041 * [コマンド1] CsvNavigateCommand(name列) + ChainRule(TrimRule → LowerCaseRule) 042 * 商品名の前後空白を除去して小文字に統一 043 * ↓ 044 * [コマンド2] CsvNavigateCommand(price列) + カスタムIRule実装クラス(PriceFormattingRule) 045 * 価格を "¥1,234" 形式にフォーマット 046 * ↓ 047 * [コマンド3] CsvNavigateCommand(category列) + ラムダIRule 048 * カテゴリを大文字に変換(ラムダで実装) 049 * </pre> 050 * 051 * <p><b>シナリオ(JSON 3段パイプライン):</b> 052 * 053 * <pre> 054 * [コマンド1] JsonNavigateCommand($.name) + ChainRule(TrimRule → LowerCaseRule) 055 * [コマンド2] JsonNavigateCommand($.category) + CamelToSnakeCaseRule(組み込みRuleの例) 056 * [コマンド3] JsonNavigateCommand($.sku) + ラムダIRule("SKU-" プレフィックス付与) 057 * </pre> 058 * 059 * <p><b>シナリオ(XML 2段パイプライン):</b> 060 * 061 * <pre> 062 * [コマンド1] XmlNavigateCommand(product/name) + ChainRule(TrimRule → LowerCaseRule) 063 * [コマンド2] XmlNavigateCommand(product/category) + CamelToSnakeCaseRule 064 * </pre> 065 */ 066public class NavigateAndRuleExample { 067 068 private static final Logger log = LoggerFactory.getLogger(NavigateAndRuleExample.class); 069 070 /** 071 * @param args コマンドライン引数(未使用) 072 * @throws IOException I/O エラー 073 */ 074 public static void main(String[] args) throws IOException { 075 log.info("=== 例2: Navigate系コマンド × IRule ==="); 076 077 csvPipeline(); 078 jsonPipeline(); 079 xmlPipeline(); 080 } 081 082 // --------------------------------------------------------------------------- 083 // CSV パイプライン 084 // --------------------------------------------------------------------------- 085 086 private static void csvPipeline() throws IOException { 087 log.info("--- CSV パイプライン ---"); 088 089 String csv = 090 "name,price,category\n" 091 + " Laptop Computer ,128000,electronics\n" 092 + " Wireless Mouse ,3200,accessories\n"; 093 094 log.info("入力 CSV:\n{}", csv); 095 096 // コマンド1: 組み込み Rule を ChainRule で連鎖させる 097 // ChainRule.of() に複数の IRule を渡すと、先頭から順番に適用される 098 IRule trimAndLower = ChainRule.of(new TrimRule(), new LowerCaseRule()); 099 100 // コマンド2: IRule をクラスで実装した例 101 // 複雑な変換ロジックはクラスで実装することで可読性・テスト性が高まる 102 IRule priceFormatter = new PriceFormattingRule(); 103 104 // コマンド3: ラムダで IRule を実装した例 105 // シンプルな変換であればラムダで十分 106 IRule upperCase = s -> s.toUpperCase(); 107 108 // PriceFormattingRule が "¥128,000" を出力する(カンマを含む)。 109 // CsvNavigateCommand は RFC 4180 に従い、カンマを含む値を二重引用符で囲む。 110 String csvExpected = 111 "name,price,category\r\nlaptop computer,\"¥128,000\",ELECTRONICS\r\nwireless mouse,\"¥3,200\",ACCESSORIES\r\n"; 112 log.info("期待値:\n{}", csvExpected); 113 114 ByteArrayOutputStream csvOut = new ByteArrayOutputStream(); 115 StreamConverter.create( 116 CsvNavigateCommand.create(CSVPath.of("name"), trimAndLower), 117 CsvNavigateCommand.create(CSVPath.of("price"), priceFormatter), 118 CsvNavigateCommand.create(CSVPath.of("category"), upperCase)) 119 .run(new ByteArrayInputStream(csv.getBytes(StandardCharsets.UTF_8)), csvOut); 120 log.info("出力:\n{}", csvOut.toString(StandardCharsets.UTF_8)); 121 } 122 123 // --------------------------------------------------------------------------- 124 // JSON パイプライン 125 // --------------------------------------------------------------------------- 126 127 private static void jsonPipeline() throws IOException { 128 log.info("--- JSON パイプライン ---"); 129 130 String json = 131 """ 132 {"name":" Laptop Computer ","category":"personalComputer","sku":"lp001"} 133 """; 134 135 log.info("入力 JSON:\n{}", json); 136 137 // 組み込み Rule: ChainRule で TrimRule → LowerCaseRule 138 IRule trimAndLower = ChainRule.of(new TrimRule(), new LowerCaseRule()); 139 140 // 組み込み Rule: CamelToSnakeCaseRule(camelCase → snake_case) 141 // builder() で細かい設定も可能 142 IRule camelToSnake = CamelToSnakeCaseRule.create(); 143 144 // ラムダ Rule: SKU にプレフィックスを付与 145 IRule addSkuPrefix = s -> "SKU-" + s.toUpperCase(); 146 147 String jsonExpected = 148 """ 149 {"name":"laptop computer","category":"personal_computer","sku":"SKU-LP001"} 150 """; 151 log.info("期待値:\n{}", jsonExpected); 152 153 ByteArrayOutputStream jsonOut = new ByteArrayOutputStream(); 154 StreamConverter.create( 155 JsonNavigateCommand.create(TreePath.fromJson("$.name"), trimAndLower), 156 JsonNavigateCommand.create(TreePath.fromJson("$.category"), camelToSnake), 157 JsonNavigateCommand.create(TreePath.fromJson("$.sku"), addSkuPrefix)) 158 .run(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)), jsonOut); 159 log.info("出力:\n{}", jsonOut.toString(StandardCharsets.UTF_8)); 160 } 161 162 // --------------------------------------------------------------------------- 163 // XML パイプライン 164 // --------------------------------------------------------------------------- 165 166 private static void xmlPipeline() throws IOException { 167 log.info("--- XML パイプライン ---"); 168 // XmlNavigateCommand は JSON と同様に XML 全体構造を保持しながら特定パスの値を変換する。 169 // 複数フィールドを別々のコマンドで変換することも可能。 170 // コマンド1: product/name の前後空白をトリムして小文字化 171 // コマンド2: product/category をスネークケースに変換 172 173 String xml = 174 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" 175 + "<product>\n" 176 + " <name> Laptop Computer </name>\n" 177 + " <category>personalComputer</category>\n" 178 + " <sku>lp001</sku>\n" 179 + "</product>\n"; 180 181 log.info("入力 XML:\n{}", xml); 182 183 String xmlExpected = 184 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" 185 + "<product>\n" 186 + " <name>laptop computer</name>\n" 187 + " <category>personal_computer</category>\n" 188 + " <sku>lp001</sku>\n" 189 + "</product>\n"; 190 log.info("期待値(XML 全体を保持しながら name と category を変換):\n{}", xmlExpected); 191 192 ByteArrayOutputStream xmlOut = new ByteArrayOutputStream(); 193 StreamConverter.create( 194 XmlNavigateCommand.create( 195 TreePath.fromXml("product/name"), 196 ChainRule.builder().addRule(new TrimRule()).addRule(new LowerCaseRule()).build()), 197 XmlNavigateCommand.create( 198 TreePath.fromXml("product/category"), CamelToSnakeCaseRule.create())) 199 .run(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), xmlOut); 200 log.info("出力:\n{}", xmlOut.toString(StandardCharsets.UTF_8)); 201 } 202 203 // --------------------------------------------------------------------------- 204 // カスタム IRule 実装クラスの例 205 // --------------------------------------------------------------------------- 206 207 /** 208 * 価格文字列を "¥1,234" 形式にフォーマットする Rule。 209 * 210 * <p>{@link IRule} はインターフェースなので、クラスで実装することで: 211 * 212 * <ul> 213 * <li>複雑な変換ロジックをメソッドに分割できる 214 * <li>単体テストを書きやすくなる 215 * <li>設定値をフィールドで保持できる 216 * </ul> 217 */ 218 static class PriceFormattingRule implements IRule { 219 220 @Override 221 public String apply(String input) { 222 if (input == null || input.isBlank()) { 223 return input; 224 } 225 try { 226 long price = Long.parseLong(input.trim()); 227 return String.format("¥%,d", price); 228 } catch (NumberFormatException e) { 229 // 数値でない場合はそのまま返す 230 return input; 231 } 232 } 233 } 234}