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}