チュートリアル - ワードラップ関数の作成

  1. 概要
  2. ワードラップとは
  3. 下準備
  4. 折り返し処理の実装
  5. ウィザードの実行
  6. カスタム関数の作成
    1. バグ修正 - 半角文字判定
    2. バグ修正 - 英単語の分割禁止
    3. 機能追加 - 禁則処理
      1. プロパティの追加(Javaソース)
      2. プロパティの追加(定義ファイル)
      3. 禁則処理の実装
    4. 機能追加 - コンパイル時のチェック強化
  7. 更なる改良のために

1 概要

マッパー関数の作成チュートリアルとしてここではワードラップするマッパー関数を作成してみたいと思います。

2 ワードラップとは

一般的にはワードラップと言うと英語の文章で単語が2行に分割されないような処理を指しますが、 ここでは指定幅での文章の折り返し処理全体も含めてワードラップと読んでいます。

ワープロなどのソフトではフォントサイズと幅から文章の幅を計算して折り返し幅を計算しますが、テキスト処理の場合は文字数(バイト数) で折り返し処理が行われることが多いです。
一般的にはメール送信の前に80行前後で折り返すような使われ方をします。

単純に文字数(バイト数)を数えて折り返すだけなら簡単なのですが、行頭に「。」や「、」がこないような禁則処理を入れたり英単語が分割されないような ロジックを含めることまで考えるとなかなか面倒です。

ここではまず最初に固定のバイト数によって折り返す関数を作成して徐々にプロパティを増やして機能追加していくことにします。

3 下準備

マッパー関数の作成に先立って先にテストのためのフローを作成しておきます。
今回作成するのは入力された文章を適当な所で折り返すマッパー関数なのでフローとしては

Start -> Mapper -> EndResponse

のようなフローで十分です。
マッパーの出力ストリームをTextストリームにしてそこに作成した関数の出力を差し込むようにすれば良いでしょう。
テストデータとしては普通にメールで作成する文章のようなある程度長さのある文章が良いので、ここではASTERIA WARPのWEBサイトから 製品のの紹介文を持ってくることにしました。
CONSTにテストデータの文章を貼り付け、それをJavaInterpreterを通してから、マッパーの出力ストリームに差し込みます。

ちなみに上図のCONSTの説明はワードラップされています。
こちらの方は文字数ではなくフォントサイズから1文字ずつ文字幅を計算して1行の文字数を計算しているので、計算ロジックはやや複雑ですが禁則処理は実装されていないので「、」が行頭にきていたりします。

JavaInterpreterのソースはとりあえず入力された文字列をそのまま出力することにします。ソースには次のように入力してください。

String str = in[0].strValue();
out.setValue(str);

ここまで作成できたらまずは一度フローを実行してみて正常に動作するか確認してみてください。
実行に成功すると当たり前ですがCONSTに入力した文字列がそのまま出力されます。

4 折り返し処理の実装

まずは禁則処理は無視して単純に80バイトごとに文字列に改行を差し込んでいく処理を作成してみます。
ループで1文字ずつ文字幅を数えて80バイトに達したら改行コードを挿入すればOKです。
文字列の途中に改行コードがでてきたらバイト数のカウントをリセットすることも忘れずに。

それをふまえてJavaInterpreterのソースをかくとコードは以下のようになりました。

int max = 80;
String lf = "\r\n";
boolean bchar = false;

String str = in[0].strValue();
int len = str.length();
int spos = 0;
int linelen = 0;
StringBuilder buf = new StringBuilder();
for (int i=0; i<len; i++) {
	char c = str.charAt(i);
	if (c == '\r' || c == '\n') {
		buf.append(str.substring(spos, i+1));
		spos = i+1;
		linelen = 0;
	} else {
		int clen = bchar || c < 0x7F ? 1 : 2;
		linelen += clen;
		if (linelen >= max) {
			buf.append(str.substring(spos, i));
			buf.append(lf);
			spos = i;
			i--;
			linelen = 0;
		}
	}
}
if (spos < len) {
	buf.append(str.substring(spos));
}
out.setValue(buf.toString());

ロジックについて詳しい解説を行うことはしませんが、短いソースコードなのでJavaの知識のある人であれば理解は容易だろうと思います。

最初の3行はプロパティ値の設定です。

max 折り返し桁数
lf 改行コード
bchar 文字数を数えるかバイト数を数えるかのフラグ

JavaInterpreter関数にはJavaInterpreterコンポーネントのようなユーザー定義のプロパティを設定する機能がないのでここでは固定値を設定しています。
また途中にでてくる変数「clen」が1文字のバイト数を表していますが、このロジックではUS-ASCII以外の半角文字(例えば半角カナ)は2バイトと判定されてしまいます。
これもあとでprivate関数を使うように修正しますが、とりあえずはこのままで実行してみましょう。

実行結果は以下のようになります。
(デザイナーの画面上ではフォントが等幅ではないためわかりづらいのでpreタグで示します。)

「ASTERIA WARP(アステリア ワープ)」は、企業の情報システムを短期間に構築するた
めのデータ連携ソリューションスイート。スピーディーで戦略的なビジネス展開を目指
す企業システム構築を実現します。

アプリケーション、プロセス、データなどを、組織の枠を超えて連携する仕組みとして
、ESP(Enterprise Service Pipeline)を採用。ブラウザ操作でノン・デベロップメン
トにデータ連携を実現する『パイプライン機能』と、GUIベースの開発環境で簡易にデー
タ連携を実現する『フロー機能』により、企業の様々なビジネス上の課題解決をもたら
すシステムづくりを支援します。

新規システム構築から、既存システムの再構築まで、データ連携の決定版!「ASTERIA W
ARP」を是非ご利用ください。

80バイトでの改行はうまくいっていますが、行頭に「、」がきたり「WARP」という単語が途中で切れたりしています。
JavaInterpreterでのプロト作成はここまでにして、ここから先はウィザードでJavaのソースコードを生成した後に修正していきます。

5 ウィザードの実行

動作が確認できたらJavaInterpreterの右クリックメニュー「カスタム関数の作成」からこの関数をカスタムコンポーネント化してみます。

基本設定(1ページ目)
マッパー関数名 WordWrap
表示名 (なし)
アイコン wordwrap.png(SDKのsampleフォルダにあります。)
初期表示タブ 文字列
ツールチップ 入力文字列を折り返します

名前やツールチップは自由に付けていただいて構いません。
またあとから定義ファイルやソースコードを直接変更することもできます。

Javaクラス設定(2ページ目)
クラス名 WordWrapFunction
パッケージ名 com.infoteria.sample.function
保存先フォルダ (任意のフォルダ)

サンプルのjarファイル内には上記のパッケージ名とクラス名が使用されていますが、 自分で作成される場合はJavaのパッケージ名とクラス名は変更した方が良いでしょう。

入出力設定(3ページ目)
最小入力数 1
最大入力数 2
出力数 1

この関数では折り返す文字数(バイト数)の指定を2本目の入力リンクで置換できることにします。
ここまでに作成したJavaのソースコードにはそのためのコードは含まれていませんが、設定ファイルの定義だけはここで先に最大入力数として 「2」を指定しておきます。

プロパティ設定(4ページ目)
プロパティ名 LineLen
表示名 1行の文字数
int
エディタ (なし)
デフォルト値 80
必須 true
ツールチップ 1行の文字数を指定します
プロパティ名 LineFeed
表示名 改行コード
choice
エディタ (なし)
デフォルト値 <CRLF>
&lt;LF>
<CR>
必須 true
ツールチップ 改行コードを指定します
プロパティ名 Mode
表示名 文字数の単位
choice
エディタ (なし)
デフォルト値 String=文字
Binary=バイト
必須 true
ツールチップ 文字数の数え方を指定します

JavaInterpreter関数にはプロパティを定義する機能はないので初期状態ではこの画面には何も設定されていません。
JavaInterpreterのソースコードでは1行の文字数や改行コードは固定値としましたが、これからコードを変更するのでウィザードの生成する ソースコードにその情報を反映するためあらかじめプロパティを設定しておきます。

上記の設定では「文字数」をint型に、「改行コード」と「文字数の単位」をchoice型としています。
choice型の場合はデフォルト値の列に改行区切りで選択肢となる値を列挙します。
表示名を使用する場合は、「<実名>=<表示名>」のように「=」で実名と表示名を区切ります。

※ ASTERIA WARP 4.0.1以前のバージョンでは「<>」の扱いや表示名のあるchoiceのデフォルト値設定にバグがあります。
ウィザードによって設定ファイルを生成後に下記の部分を修正してください。

    <Property name="LineFeed"  displayName="改行コード"  type="choice"   choiceItem="&lt;CRLF&gt;&#xa;&lt;LF&gt;&#xa;&lt;CR&gt;"
                 tooltip="改行コードを指定します"  required="true"  >&lt;CRLF&gt;</Property>
    <Property name="Mode"  displayName="文字数の単位"  type="choice"   choiceItem="String=文字&#xa;Binary=バイト"  tooltip="文字数の数え方を指定します"  
                 required="true"  >Binary</Property>

ソース設定(5ページ目)
(省略)

JavaInterpreterで設定したソースコードが画面に反映されています。
特別なことは何もしていないのでここでは最初に表示されている内容を修正する必要はありません。

ビルド設定(6ページ目)
build.xmlを生成する チェック
jarファイル名 fcsample.jar
バージョン番号 1.0
すぐにビルドを実行 オフ
ASTERIA WARPサーバーのパス [INSTALL_DIR]を指定

マシンにANTがインストールされているなら即時実行することもできますが、ここではあとからコマンドラインでビルドすることにします。
同一マシン上にASTERIA WARPサーバーがインストールされているのであればここでそのパスを指定しておくとANTのbuild.xmlに作成したマッパー関数jarファイルの コピータスクとフローサービスの再起動タスクが組み込まれるので今後の開発サイクルが多少楽になります。

6 カスタム関数の作成

生成されたWordWrapFunction.javaをエディタで開くとexecuteメソッド内のコードはJavaInterpreterで記述したコードそのままであることが確認できます。
またinternalInitメソッド内でウィザードで定義したプロパティも追加されています。
ソースコードの方では定義内容のうちプロパティ名、データ型、必須の3項目しか使用されていませんが、表示名やツールチップの設定は定義ファイル(WordWrapFunction.xmf) の方に反映されます。

このコードはそのままでもコンパイル/実行することができますが、JavaInterpreter内で固定値を設定していた変数だけはちゃんとプロパティから値を取得するように修正しておきましょう。
関数内でプロパティ値を取得する場合はgetPropertyXXXXメソッドを使用します。また第2引数にinの配列を、第3引数に配列内での位置を指定すると配列のサイズが第3引数の指定よりも 大きかった場合(つまり入力リンク数が指定の配列位置よりも多かった場合)に配列内の値をプロパティ値の替わりに使用することもできます。
修正したコードは以下のようになります。

package com.infoteria.sample.function;

import com.infoteria.asteria.flowengine2.execute.ExecuteContext;
import com.infoteria.asteria.flowlibrary2.mapper.Function;
import com.infoteria.asteria.flowlibrary2.mapper.MapperException;
import com.infoteria.asteria.value.Value;

public class WordWrapFunction extends Function {
	
	private static final String FUNCTION_NAME   = "WordWrap";
	
	public String getFunctionName() { return FUNCTION_NAME;}
	
	protected void internalInit() {
		registProperty("LineLen", Value.TYPE_INTEGER, true);
		registProperty("LineFeed", Value.TYPE_STRING, true);
		registProperty("Mode", Value.TYPE_STRING, true);
	}
	
	public int getMinInputCount() { return 1;}
	public int getMaxInputCount() { return 2;}
	
	public void execute(ExecuteContext context, Value[] in, Value out) throws MapperException {
		int max = (int)getPropertyInteger("LineLen", in, 1);
		String lf = "\r\n";
		String strLineFeed = getPropertyString("LineFeed");
		if ("<CR>".equals(strLineFeed))
			lf = "\r";
		else if ("<LF>".equals(strLineFeed))
			lf = "\n";
		boolean bchar = "String".equals(getPropertyString("Mode"));
		
		String str = in[0].strValue();
		int len = str.length();
		int spos = 0;
		int linelen = 0;
		StringBuilder buf = new StringBuilder();
		for (int i=0; i<len; i++) {
			char c = str.charAt(i);
			if (c == '\r' || c == '\n') {
				buf.append(str.substring(spos, i+1));
				spos = i+1;
				linelen = 0;
			} else {
				int clen = bchar || c < 0x7F ? 1 : 2;
				linelen += clen;
				if (linelen >= max) {
					buf.append(str.substring(spos, i));
					buf.append(lf);
					spos = i;
					i--;
					linelen = 0;
				}
			}
		}
		if (spos < len) {
			buf.append(str.substring(spos));
		}
		out.setValue(buf.toString());
		
	}
}

作成した関数をサーバーにインストールしたらテストで作成したフローを複製して、JavaInterpreterを作成した関数に置き換えてテストしてみてください。
デフォルトのプロパティ設定でJavaInterpreterの時と同じ結果が得られると思います。
またプロパティ値を変更したり、1行の文字数を2本目の入力として関数に差し込んだ場合も指定の値が有効になります。

次節からはこのコードをベースにJavaInterpreterでは後回しにしていたバグ修正や機能追加を行っていきます。

6.1 バグ修正 - 半角文字判定

上のコードでは半角文字の判定を0x7F以下と判定していましたが、このコードでは半角カナが半角文字と判定されないので修正が必要です。
ここでは日本語(JIS X 201)環境に限定してcharが半角文字かどうかを判定する関数を追加することにします。

	//executeメソッド内の呼び出し箇所
	int clen = bchar || isHalfWidth(c) ? 1 : 2;
	...
	
	/** 半角文字の判定 */
	private static boolean isHalfWidth(char c) {
		return c < 0x7F || (c >= 0xFF61 && c <= 0xFF9F);
	}

JavaInterpreterではすべてのロジックをひとつのメソッド内に記述する必要がありましたが、Javaソース生成後は自由に関数を追加することが出来ます。
アジア圏の別の文字も半角文字と判定する必要がある場合はこの関数だけを修正すればOKです。

6.2 バグ修正 - 英単語の分割禁止

今回使用したテストデータの実行結果では「WARP」という英単語が2行に分割されてしまっていました。
これを回避するために次のようなロジックを組み込むことにします。

厳密な仕様とは言えませんが、実際問題としてはこの仕様で十分でしょう。
むしろ問題は何を「英数字」と判定するかの方ですが、ここでは全角半角の区別なくアルファベット、数字、「-(ハイフン)」、「_(アンダースコア)」を 英数字と判定することにします。

	//executeメソッド内の文字列追加ロジック修正
	if (linelen >= max) {
		String line = str.substring(spos, i);
		if (isAlpha(c)) {
			String word = getLastWord(line);
			if (word != null) {
				int wordlen = word.length();
				line = line.substring(0, line.length() - wordlen);
				i -= wordlen;
				clen = bchar || isHalfWidth(word.charAt(0)) ? 1 : 2;
			}
		}
		buf.append(line);
		buf.append(lf);
		spos = i;
		i--;
		linelen = 0;
	}
	...
	
	/** 英数字の判定 */
	private static boolean isAlpha(char c) {
		//アルファベット(半角・全角)
		if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
		    (c >= 0xFF21 && c <= 0xFF3A) || (c >= 0xFF41 && c <= 0xFF5A))
			return true;
		//数字(半角・全角)
		if ((c >= '0' && c <= '9') || (c >= 0xFF10 && c <= 0xFF19))
			return true;
		//ハイフンとアンダースコア
		if (c == '-' || c == '_' || c == 0xFF0D || c == 0xFF3F)
			return true;
		
		return false;
	}
	
	/** 行末のワードを取得 */
	private static String getLastWord(String line) {
		String ret = null;
		int last = line.length() - 1;
		for (int i=last; i>=0; i--) {
			char c2 = line.charAt(i);
			if (!isAlpha(c2))
				return i == last ? null : line.substring(i+1);
		}
		return null;
	}

テストデータを適当な英文に変更して実行すれば英単語の分割禁止が適切に行われていることが確認できます。

6.3 機能追加 - 禁則処理

ネットで禁則処理の実装方法を調べてみると、ほとんどの場合は次の3つの概念の組み合わせで処理されているようです。

細かいことを言うと禁則文字が連続する場合のぶらさげ方や追い出し方の制御にも何パターンかあるんですが、 ここでは上記3つをプロパティとした上でできるだけシンプルな形で実装することにします。

6.3.1 プロパティの追加(Javaソース)

Javaソース側でプロパティを追加するにはinternalInitメソッドの中でregistPropertyメソッドを実行します。

	protected void internalInit() {
		registProperty("LineLen", Value.TYPE_INTEGER, true);
		registProperty("LineFeed", Value.TYPE_STRING, true);
		registProperty("Mode", Value.TYPE_STRING, true);
		registProperty("LineHeadWrap", Value.TYPE_STRING, false);//行頭禁則文字
		registProperty("LineEndWrap", Value.TYPE_STRING, false);//行末禁則文字
		registProperty("Dangling", Value.TYPE_BOOLEAN, false);//ぶらさげを許可
	}

これでexecuteなどのメソッド内からgetPropertyXXXXメソッドでデザイナーで設定されたプロパティ値に アクセスできるようになります。

6.3.2 プロパティの追加(定義ファイル)

定義ファイル側でプロパティを追加するにはProperty要素を追加します。

	<Property name="LineHeadWrap"  displayName="行頭禁則文字"  type="string"   
	    tooltip="行頭禁則文字を指定します"  >、。,.?!)〕]}〉》」』】、。,.?!)]}」゙゚</Property>
	<Property name="LineEndWrap"  displayName="行末禁則文字"  type="string"   
	    tooltip="行末禁則文字を指定します"  >(〔[{〈《「『【([{「</Property>
	<Property name="Dangling"  displayName="ぶらさげを許可"  type="boolean"   
	    tooltip="行頭禁則文字がある場合にぶらさげを許可するかどうかを指定します" >false</Property>

Javaソース側ではプロパティ名は英字で指定しましたが、デザイナーでの表示用に日本語でdisplayNameを指定します。
また関数をパレットから配置した直後でも適切な禁則処理が行えるようにプロパティ値にはあらかじめデフォルト値を指定しておきます。

6.3.3 禁則処理の実装

禁則処理のロジックの実装自体はこの文書の目的から外れるのでここでは細かい解説は行いません。
完全なソースコードはサンプルフォルダーにあるので興味のある方は参照してみてください。
(サンプルフォルダーのソースは文書内で示したコードよりもいくらか最適化されています。)

6.4 機能追加 - コンパイル時のチェック強化

この関数は1行の文字数が極端に短い場合、適切な動作を行うことができなくなります。
そこでコンパイル時に1行の文字数をチェックして10文字以下の場合はエラーにすることにします。

	protected void postCompile() throws MapperException {
		//リンクが2本ある場合はチェックを行わない
		if (getInputList().size() > 1)
			return;
		int max = (int)getPropertyInteger(PROPERTY_LINE_LEN);
		if (max < 10) {
			String msg = getMessage("1", Integer.toString(max));
			throw new MapperException(msg);
		}
	}

エラーチェックはpostCompileメソッドをオーバーライドしてそこで行います。
このメソッド内でMapperExceptionをthrowするとコンパイラにコンパイルエラーがレポートされます。 上で使用しているgetMessageメソッドは定義ファイルからメッセージを取得するためのメソッドです。
メッセージは定義ファイル内でMessage要素で定義します。

	<Message key="1">1行の文字数が短すぎます : %1</Message>

Message要素では置換文字列を3つまで%1~%3で示すことが出来ます。

7 更なる改良のために

禁則処理は文化によって様々なローカルルールがあるので、場合によっては今回の実装では適切な処理を行えないかもしれません。
単純に禁則文字の変更だけであるなら関数でプロパティを変更するだけですが、毎回それを行うのが面倒なら定義ファイルのデフォルト値を 変更するという方法もあります。(この場合はjar自体を作り直さずともDESIGNER_HOME/confにあるxmfファイルを修正するだけでもOKです。)
この実装では処理しきれないようなルールがある場合は是非このソースの改良にトライしてみてください。