Cloud Data Integrationのマッピングでサロゲートペア文字を含むCSVファイルを扱ってみる

この記事は インフォマティカ [Advent Calendar 2023](https://qiita.com/advent-calendar/2023/informatica) Day 24 として書いています。

最近ふと『これならサロゲートペア文字を扱えるんじゃないっ?!しかも簡単に!!』と思いついて試したらできちゃった話を紹介したいと思います。

### はじめに

CDI(Cloud Data Integration)ではCSVファイルを扱う場合、一般的にはFlatFile接続を利用しますが、FlatFile接続を利用するとサロゲートペア文字は文字化けの対象となります。マッピングはサロゲートペア文字をサポートしていないためです。

この記事では、次のコンポーネントを利用してマッピングでサロゲートペア文字を処理した場合にどのような動作になるかを紹介します。

- FlatFile接続
- JavaTX(トランスフォーメーション)
- ISD(インテリジェント構造検出)

さらに今回はサロゲートペア文字の文字化けを回避する方法としてJavaTXを利用する実装方法についても紹介します。
なお、筆者が所属する組織では今回紹介しているJavaコードのカスタマイズ方法を提供していません。カスタマイズが必要な場合は、担当システム・プロジェクトのJava有識者にご相談いただければと思います。

### 各実装方法による動作の紹介

まずは、次の3つの実装方法別にサロゲートペア文字がどのように処理されるかを見てゆきます。

- FlatFile接続
- JavaTX(トランスフォーメーション)
- ISD(インテリジェント構造検出)

以下、今回利用するテストデータです。

```
col1,col2,col3
"01","鱸 is not Surrogate Pair",test
"02","鰯 is not Surrogate Pair",test
"03","鱮 is not Surrogate Pair",test
"04","𩸽 IS Surrogate Pair    ",test
"05","𩸽 IS Surrogate Pair    ","test"
"06","𩸽 IS Surrogate Pair    ","""test"
"07","𩸽 IS Surrogate Pair    ","t""est"
"08","𩸽 IS Surrogate Pair    ","te""st"
"09","𩸽 IS Surrogate Pair    ","tes""t"
"10","𩸽 IS Surrogate Pair    ","test"""
"11","𩸽 IS Surrogate Pair    ",",test"
"12","𩸽 IS Surrogate Pair    ","test,"
```

𩸽(ほっけ)という文字がサロゲートペア文字です。こちらのデータを利用して次の2点を確認してゆきます。

- col2列 ... サロゲートペア文字を扱えるか?
- col3列 ... ダブルクォート内のエスケープ処理が適切に行われるか?


#### FlatFile接続を利用した場合

FlatFile接続を使ってパススルーマッピングを実行した場合、次の出力が得られました。
```
"col1","col2","col3"
"01","鱸 is not Surrogate Pair","test"
"02","鰯 is not Surrogate Pair","test"
"03","鱮 is not Surrogate Pair","test"
"04","੸ݠIS Surrogate Pair    ","test"
"05","੸ݠIS Surrogate Pair    ","test"
"06","੸ݠIS Surrogate Pair    ","""test"
"07","੸ݠIS Surrogate Pair    ","t""est"
"08","੸ݠIS Surrogate Pair    ","te""st"
"09","੸ݠIS Surrogate Pair    ","tes""t"
"10","੸ݠIS Surrogate Pair    ","test"""
"11","੸ݠIS Surrogate Pair    ",",test"
"12","੸ݠIS Surrogate Pair    ","test,"
```
サロゲートペア文字が文字化けしていることを確認できます(想定動作)。
エスケープ処理は期待とおりの動作を確認できます。

:::note
『パススルーマッピング』とはSource/TargetTXのみを有するマッピングです。
:::

#### JavaTXを利用した場合

後述する実装方法でJavaTXを利用した場合、次の出力が得れました。
```
"path","col1","col2","col3","error"
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","01","鱸 is not Surrogate Pair","test",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","02","鰯 is not Surrogate Pair","test",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","03","鱮 is not Surrogate Pair","test",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","04","𩸽 IS Surrogate Pair    ","test",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","05","𩸽 IS Surrogate Pair    ","test",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","06","𩸽 IS Surrogate Pair    ","""test",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","07","𩸽 IS Surrogate Pair    ","t""est",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","08","𩸽 IS Surrogate Pair    ","te""st",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","09","𩸽 IS Surrogate Pair    ","tes""t",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","10","𩸽 IS Surrogate Pair    ","test""",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","11","𩸽 IS Surrogate Pair    ",",test",""
"/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv","12","𩸽 IS Surrogate Pair    ","test,",""
```
実装の都合上、path列・error列が加わっておりますが、サロゲートペア文字を扱えている動作を確認できます(扱えちゃってる!!)。
エスケープ処理も期待とおりです。

#### ISDを利用した場合

ISDを利用した場合、次の出力が得られました。
```
"col1","col2","col3"
"01","鱸 is not Surrogate Pair","test"
"02","鰯 is not Surrogate Pair","test"
"03","鱮 is not Surrogate Pair","test"
"04","𩸽 IS Surrogate Pair","test"
"05","𩸽 IS Surrogate Pair","test"
"06","𩸽 IS Surrogate Pair","""""test"
"07","𩸽 IS Surrogate Pair","t""""est"
"08","𩸽 IS Surrogate Pair","te""""st"
"09","𩸽 IS Surrogate Pair","tes""""t"
"10","𩸽 IS Surrogate Pair","test"""""
"11","𩸽 IS Surrogate Pair",",test"
"12","𩸽 IS Surrogate Pair","test,"
```
こちらもサロゲートペア文字を扱えている動作を確認できます(扱えちゃってる!!)。
一方でエスケープ処理は、ISDの設定に起因しているかは確認できておりませんが、次の点で他2つの実装方法とは動作が異なるようです(2023年12月時点の動作)。

- エスケープ処理(ダブルクォテ―ションが想定の倍、出力される)
- col2列末尾の半角スペースの扱い

### JavaTXの実装方法

3つの実装方法の中で、現時点(2023年12月時点)で最も的確にサロゲートペア文字を扱えていると思われるJavaTXの実装方法を紹介します。

:::note warn
マッピングはサロゲートペア文字を前提とした動作確認が行われていません。例えば、サロゲートペア文字に対して式TXでsubstr関数を利用しても期待しない動作となることを確認しています。
また、今回のJavaTXの実装例ではFlatFile接続でサポートしているCSVの要素中に改行を含むデータの処理には対応しておりません。
:::

:::note
JavaTXを利用するには次のナレッジに記載の様に、予め環境変数[JDK_HOME]を設定後にSecure Agentの再起動が必要です。
[HOW TO: Setup JDK_HOME environment variable in Windows and Linux agent environments ?](https://knowledge.informatica.com/s/article/564913?language=en_US)
:::

:::note
以下はJavaTXの使い方に関する製品ドキュメントのリンクです。
[日本語ドキュメント : Javaトランスフォーメーション](https://docs.informatica.com/ja_jp/integration-cloud/data-integration/current-version/_transformations_cloud-data-integration_current-version_ditamap/GUID-56C5D3F1-86EE-4C9D-8AFB-380500211E8A.html)
[英語ドキュメント : Java transformation](https://docs.informatica.com/integration-cloud/data-integration/current-version/transformations/java-transformation.html)
:::


#### マッピングの全体像
マッピングでは以下のとおり、SourceTX・JavaTX・TargetTXを利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/249eff05-4814-42a1-1499-d5aaefb45a4a.png)
処理の流れは次のとおりです。

- SourceTX ... CSVファイルのパスを取得する
- JavaTX ... CSVファイルを読み込み、データ行を生成する
- TargetTX ... 任意の接続を利用してデータ連携を実装する(今回はFlatFile接続を利用)

#### SourceTXの設定

SourceTXではFlatFile接続を利用して下記形式のSourceファイルを指定します。

- フィールドは1つ、pathという列名を指定する
- データ部は1行のみ、読み込み対象のCSVファイルを指定する

以下は、今回利用したファイルの例です。
```
path
/opt/infa1/ff/src_javaTx_SurrogatePaireEx.csv
```

#### TargetTXの設定

TargetTXでは、今回はFlatFile接続を利用します。利用する接続は次のように『コードページ=UTF8』の設定であることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/19ebb12b-9ac0-c7a5-51a5-0fec44e4425d.png)


#### JavaTXの設定 - 出力フィールドタブ
JavaTXは2つの画面を対象に操作します。
1つめは出力列を定義する出力フィールドタブです。このタブでは、CSVファイルに含まれるフィールドに加えてerrorフィールドを定義します。
以下、今回利用しているCSVファイルを利用する場合の設定例です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/555848d8-910c-4de7-3ba2-32d40964bbf1.png)

:::note
フィールドのタイプは全てStringを指定します。他のタイプを利用する場合、式TXを追加して必要なタイプに変換します。
:::

:::note warn
CSVファイルの列名と、出力フィールドは __完全に一致__ するよう設定します。
列名と出力フィールドの定義に相違がある場合、適切に動作しません。
また、CSVファイルの列名においてJavaの変数としてで利用できない文字列が含まれている場合には、予めスクリプトで変換しておきます。
:::


#### JavaTXの設定 - Javaタブ

JavaTXで操作するもう1つの画面は、Javaコードを入力するJavaタブです。JavaTXでは次の5つのセクションにJavaコードを入力します。

- パッケージのインポート(Import Package)
- ヘルパーコード(Helper Code)
- 入力行(On Row Input)
- データの終わり(At End Of Data)
- 受信トランザクション(On Receiving Transaction)

今回は前者3つのセクションに次のコードを入力します。

```java:パッケージのインポート(Import Package)
import java.lang.reflect.Field;
import java.io.File;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
```

```java:ヘルパーコード(Helper Code)
//リフレクションにより出力フィールドに定義した変数に値を設定する
private void setFieldVal(Object obj, String fieldName, Object val) {
   try {
      Field field = obj.getClass().getDeclaredField(fieldName);
      field.setAccessible(true);
      field.set(obj, val);
   } catch(Exception e) {
      logInfo("exception in Helper-setFieldVal : " + e.toString());
   }
}

//CSV形式の文字列を配列として返す
private String[] getCSVArray(String line){
   String[] arr = null;
   try{
      Pattern pattern = Pattern.compile(",(?=(([^\"]*\"){2})*[^\"]*$)"); //カンマ区切りで文字列配列を取得する
      arr = pattern.split(line, -1);
      for (int i = 0; i < arr.length; i++) {       //CSVの各要素を対象に処理
         String data = arr[i].trim();              //前後のスペースを除去
         pattern = Pattern.compile("^\"(.*?)\"$"); //前後のテキスト修飾子(")を除去
         Matcher matcher = pattern.matcher(data);
         if(matcher.find()){
            data = matcher.group(1);
         }
         pattern = Pattern.compile("\"\"");        //エスケープされた文字を解除(""⇒")
         matcher = pattern.matcher(data);
         data = matcher.replaceAll("\"");  
         arr[i] = data;  
      }
   }catch(Exception e){
      logInfo("exception in Helper-getCSVArray : " + e.toString());
   }
   return arr;
}
```

```java:入力行(On Row Input)
//メイン処理
try{
   File fileIN = new File(path);
   BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(fileIN),StandardCharsets.UTF_8));
   String line;
   String data[];
   String header[] = getCSVArray(bufferedReader.readLine()); //ヘッダ部を配列として取得
   while((line = bufferedReader.readLine()) != null){        //データ部を取得
      try{
         error = "";                               //error列の初期化
         data = getCSVArray(line);                 //データ部の文字列をCSVとして配列データに変換
         for (int i = 0; i < header.length; i++) { //列名をもとに
            setFieldVal(this, header[i], data[i]); //出力フィールドに各列の値を設定
         }
         generateRow();                            //データ行を生成
      }catch(Exception e){                         //例外発生時、error列に例外情報を設定
         error = "exception : " + e.toString() + ", data : " + line;
         try{
            generateRow();
         }catch(Exception ex){
            logInfo(error);
         }
      }
   }
}catch(Exception e){
   logInfo("exception in OnRowInput : " + e.toString());
}
```

Javaコードの入力完了後、ランタイム環境を選択してコンパイルを実行します。正常にコンパイルが完了すると『コンパイルが成功しました』と表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/e99afe3f-3e33-011f-b67d-0040b0c5ab11.png)


:::note
CSVファイルのレイアウトに合わせてJavaコードをカスタマイズする必要はありません。
:::

:::note
今回の実装例では、行データの生成時に例外が発生した場合、error列に例外情報を設定しています。式TXを追加して、このerror列を評価することで例外のあった行データをエラー行として扱うことも可能です(iif/error関数を利用する)。
:::

### おわりに

今回はマッピングでサロゲートペア文字を扱った場合にどのような動作になるか、そしてJavaTXを使った実装例を紹介しました。
マッピングはサロゲートペア文字を前提とした動作確認が行われていません。そのためサロゲートペア文字を扱う場合には色々な制約が出てくることでしょう。一方で、プロジェクトでサロゲートペア文字を __どうしても扱いたい場合__ には、今回紹介したJavaTXの実装例が1つの選択肢にはなるのではと期待しています。

### 参照情報

https://datatracker.ietf.org/doc/html/rfc4180