プラグイン開発者ガイド

  1. プラグインとは
    1. プラグイン作成時の注意
    2. JDK・クラスパス
    3. デザイナーのコンソール
  2. プラグインの種類
    1. プロパティエディタとは
    2. プロパティリスナーとは
    3. コンポーネントエディタとは
    4. 外部アプリケーション起動とは
    5. プラグイン関連クラスのクラス図
  3. プロパティエディタの作成
    1. ButtonTextCellEditor
    2. ButtonCellEditor
    3. 実装例
    4. 定義ファイルへの組み込み
  4. Propertyの使い方
    1. setterメソッドとUndo
  5. プロパティリスナーの作成
    1. 実装例
    2. 定義ファイルへの組み込み
  6. MetaDataの実装
  7. ストリーム情報の編集
    1. 実装例
    2. XMLFieldDefinition作成時の注意
    3. フィールド定義作成の別法
    4. プロパティリスナーでのUndoの扱い方
  8. コンポーネントエディタの作成
    1. 実装例
    2. コンポーネント(マッパー関数)からの情報の取得と設定
    3. CategoryPropertyの使い方
    4. 定義ファイルへの組み込み
  9. 汎用コネクションとpluginCall
    1. サーバー側サンプル
    2. デザイナー側サンプル
  10. 標準プラグインの自作コンポーネントへの組み込み
    1. SQLBuilder
      1. SQLBuilderを使用するコンポーネントのプロパティ
      2. SQLBuilderの使用例
      3. SQL文のパース
    2. テーブル選択ダイアログ
      1. テーブル選択ダイアログを使用するコンポーネントのプロパティ
      2. テーブル選択ダイアログの使用例
  11. 外部アプリケーション起動の作成

1 プラグインとは

プラグインとはデザイナーの動作を拡張し、コンポーネントやマッパー関数のプロパティ設定などの補助をするモジュールのことです。
コンポーネントやマッパー関数のクラスがフローの実行時にサーバー側で動作するモジュールであるのに対し、 プラグインはフローの設計時にデザイナー側で動作するのでフローの実行には影響を与えません。
プラグインはコンポーネント/マッパー関数の作成で必ず作らなければならないというものではありませんが、 多機能なコンポーネント/マッパー関数を作成する場合プラグインを作成するとデザイナーの操作性が向上し、 フロー作成の効率が大幅にアップすることもあります。

1.1 プラグイン作成時の注意

プラグイン作成では多くのクラスを使用します。
それらのクラス群はサーバーで使用しているクラスと同一の場合もありますが、ほとんどの場合プラグインで使用する クラスとサーバーで使用するクラスは全く別物です。

たとえばプラグインでも「Component」というクラスを使用しますが、そのパッケージは

com.infoteria.asteria.flowbuilder2.component.Component

であり、サーバーで使用するクラス

com.infoteria.asteria.flowengine2.flow.Component

とは全く別のクラスです。
後者はサーバー上で実際にコンポーネントの行う処理を実行するクラスですが、 前者はデザイナーで使用されるxfpに保存されているコンポーネントの内容をラップしたクラスです。

1.2 JDK・クラスパス

プラグインの開発はJDK6.0以降の環境で行ってください。
また、「DESIGNER_HOME/lib」にある以下のjarファイルにクラスパスを通してください。

ほとんどの場合プラグインはそれを使用するサーバー側のクラスと同時に開発され、同じjarファイルに含められます。
そのため実際にはコンポーネント(またはマッパー関数)の作成に必要なjarファイルもクラスパスに含めることになります。

作成するプラグインの内容によってはさらに別のjarファイルがコンパイル/実行に必要になる場合があります。
その場合、flow-ctrlのshowlibコマンドを使用することで必要とするクラスがどのjarに含まれているかを確認することができます。


//showlibコマンドによって指定のクラスがどのjarに含まれているかが表示されます。
>showlib com.infoteria.asteria.flowlibrary2.stream.StreamDataXML
jar:file:C:\Program Files\asteriawarp\flow\lib\flowengine2.jar!/com/infoteria/asteria/flowlibrary2/stream/StreamDataXML.class

必要に応じてここで表示されたjarファイルにもクラスパスを通してください。
実際にはウィザードで生成されるANT用のbuild.xmlでは「DESIGNER_HOME/lib/**/*.jar」にクラスパスが通っているので、通常はbuild.xmlの設定を変更する必要はありません。

1.3 デザイナーのコンソール

プラグイン作成時にコード中に埋めた「System.out.println」やExceptionのStackTraceはデザイナーのコンソールに出力されます。
デザイナーのコンソールはデザイナー起動時にCTRLキーを押しながら起動することで表示されます。

2 プラグインの種類

現在作成可能なプラグインの種類は次の4つです。

以下にそれぞれのプラグインの概要を説明します。

2.1 プロパティエディタとは

コンポーネントやマッパー関数がどのようなプロパティを持っているかは、 定義ファイルのProperty要素で定義されます。

プロパティの型は定義ファイル内でtype属性によって示されます。
int型のプロパティの場合は数値しか入力できす、 choice型のプロパティではドロップダウンリストからプロパティ値が選択できるようになっています。 またdate型やdatetime型のプロパティのように日付を選択するカレンダーが表示されるものもあります。

つまりプロパティの型によって値を設定するためのUIが決まっています。
そのプロパティ値を設定するためのUIがプロパティエディタです。

通常プロパティ型とプロパティエディタは1対1で対応していますが、 string型のようにeditor属性でプロパティエディタを選択できるものもあります。

string型のプロパティエディタ
editor属性値 プロパティエディタ
デフォルト(省略時) 1行テキストダイアログ
multiline 複数行テキストダイアログ
condition 条件式の補完機能つき1行テキストダイアログ

もっともわかりやすい例はBranchStartコンポーネントの条件式プロパティで使用されている 「condition」エディタでしょう。
このエディタは「$」や「.」をタイプした時に条件式の補完をしてくれます。
このようにプロパティ値を設定するための補助的な機能を付加したUIを自作できるプラグインが プロパティエディタです。

2.2 プロパティリスナーとは

プロパティリスナーはJavaプログラムで日常的に使用されるEventListenerの一種で、 プロパティ値の変更に反応するリスナーです。

プロパティ値の設定前後でイベントが発生するのでそこで何かしらの処理を行うことができます。

PropertyListenerの典型的な実装例としては、プロパティ値の変更に伴い別のプロパティの表示状態など をコントロールできるSimplePropertyControllerがあります。

2.3 コンポーネントエディタとは

コンポーネントエディタとは複数のプロパティ値やフィールド定義などをまとめて設定できるように したものです。
コンポーネントエディタを使用している例としては

などがあります。

2.4 外部アプリケーション起動とは

外部アプリケーション起動はその名のとおり外部アプリケーションを起動するためのプラグインです。
外部アプリケーション起動での処理の流れは以下のようになります。

  1. プロパティ値などのコンポーネント(マッパー関数)の設定情報をファイルに書き出す
  2. 外部アプリケーションを起動し1で書き出したファイルを編集
  3. 2で編集/保存された内容をコンポーネント(マッパー関数)に設定しなおす

外部アプリケーション起動を使用した例としてはExcelBuilder、PDFBuilderなどがあります。

2.5 プラグイン関連クラスのクラス図

プラグイン作成に関連するクラスの簡略化したクラス図を以下に示します。


青色で表示されているのが開発可能なプラグインのインターフェースまたはベースクラスです。

緑色は標準で組み込まれているプラグイン具象クラスの一部です。
これらの中には定義ファイルに定義するだけでそのまま使用可能なものもあります。
(詳細は定義ファイルリファレンスをご覧ください。)

上の図に示されている事実を以下に簡単に説明します。

サーバー側の開発ではComponentやFunctionを継承して「FileGet」や「RDBPut」といった具体的な処理を行うクラスを作成するわけですが、 デザイナー上ではComponentやFunctionは末端の具象クラスであり、それ以上のサブクラスは基本的にありません。

といったコンポーネント固有の動作はすべてプラグインによって実現されています。
標準プラグインを定義ファイルに組み込むだけでも多くのことができますが、独自のプラグインを作成することでより細やかな設定が可能になります。

3 プロパティエディタの作成

プロパティエディタを作成するためにはPropertyEditorインターフェース を実装したクラスを作成します。
このインターフェースはSwingのTableCellEditorインターフェースを拡張し、setPropertyメソッドを 追加したものです。

新しいPropertyEditorを作成する場合にインターフェースのメソッドをすべて自分で作成しなければ ならないケースはほとんどなく、通常は次のButtonTextCellEditorかButtonCellEditorを継承して作成します。

3.1 ButtonTextCellEditor

ButtonTextCellEditorはインスペクタの編集エリア内にテキストフィールドがあり、 その脇に「...」という小さなボタンがあるタイプのプロパティエディタです。
ボタンをクリックすることでそのプロパティ値を設定するための補助UI(ほとんどの場合はダイアログ) が起動します。

ButtonTextCellEditorを継承したPropertyEditorの例としては

などがあります。

ButtonTextCellEditorを作成する場合実装しなければならないメソッドはdoButtonActionメソッド のみであり、そこにボタンがクリックされた場合の処理を記述します。

3.2 ButtonCellEditor

ButtonCellEditorはインスペクタの編集エリア全体がボタンで覆われたタイプのプロパティエディタです。
つまりインスペクタ内で値を設定することはできず、編集エリアをクリックすることでそのプロパティ値を設定するための補助UI(ほとんどの場合はダイアログ) が起動します。

ButtonCellEditorを継承したPropertyEditorの例としては

などがあります。

ButtonCellEditorを作成する場合実装しなければならないメソッドはdoButtonActionメソッド のみであり、そこにボタンがクリックされた場合の処理を記述します。

3.3 実装例

ButtonTextCellEditorでもButtonCellEditorでもdoButtonActionメソッドの実装内容はほとんど同じになります。
典型的な例としてはダイアログを表示してそこでOKボタンがクリックされた場合にプロパティに値を設定します。

sampleフォルダに1行テキストダイアログの替わりに履歴機能つきComboBoxを使用するサンプル (HistoryPropertyEditor.java)があります。
以下にそのコードの中からdoButtonActionの部分のポイントを説明します。

(コード中の「_combo」は履歴機能付きのComboBox、「_file」は履歴を保存するFileです。
完全なコードはsampleフォルダ内にあるjavaファイルを参照してください。)

	/**
	 * ボタンが押された時の処理
	 * 履歴機能付きComboBoxの載ったダイアログを表示
	 */
	protected void doButtonAction(EventObject e) {
		String value = getProperty().getValueAsString();  //1
		_combo.setSelectedItem(value);
		
		Frame owner = PluginUtil.getApplicationFrame();   //2
		String title = getDialogTitle();                  //3
		MyDialog dlg = new MyDialog(owner, title);
		try {
			dlg.setLocationRelativeTo(owner);
			dlg.setVisible(true);
			if (dlg.isOK()) {                             //DialogがOKボタンで終わったか?
				value = (String)_combo.getSelectedItem();
				_combo.updateHistory();
				setCellEditorValue(value);                //4
				stopCellEditing();                        //5
				if (_file != null)
					_combo.saveToFile(_file);
			} else
				cancelCellEditing();                      //6
		} catch (IOException ex) {
			ex.printStackTrace();                         //7
		} finally {
			dlg.dispose();
		}
	}
  1. getPropertyメソッドで編集対象となるPropertyオブジェクトが取得できます。
    プロパティ値はPropertyオブジェクトのgetValueメソッドまたは getValueAsStringメソッドで取得できます。
    (getValueメソッドによって返されるオブジェクトはプロパティ型によって異なります。
    string型のプロパティではStringオブジェクトを返しますが、 int型のプロパティではLongオブジェクトが返されます。)
  2. ダイアログを表示する際のOwnerFrameにはPluginUtil#getApplicationFrameの返り値を使用します。
    PluginUtilはプラグイン作成に有用なメソッドをまとめたユーティリティクラスです。
  3. ButtonTextCellEditor#getDialogTitleメソッドは「<プロパティ表示名>の編集」という文字列を返します。
    オーバーライドして任意の文字列を返すようにしても構いません。
  4. 値の設定にはsetCellEditorValueメソッドを使用します。
    プロパティエディタの実行コード内ではPropertyオブジェクトに直接setValue(またはsetValueAsString)メソッドで設定してはいけません。
    (Undoが正しく動作しなくなります。)
  5. 値を設定した場合はstopCellEditingメソッドで編集操作を完了します。
  6. 編集をキャンセルした場合はcancelCellEditingメソッドで編集操作を完了します。
  7. StackTraceはデザイナーのコンソールに出力されます。

3.4 定義ファイルへの組み込み

作成したプロパティエディタを定義ファイルに組み込むには対象のProperty要素にeditorClass属性で指定します。


<Property displayName="ファイルパス" name="FilePath" required="true" toolTip="FilePath" type="string" 
    editorClass="com.infoteria.asteria.flowbuilder2.plugin.HistoryPropertyEditor" filename="exe.txt"/>

HistoryPropertyEditorはStringを扱うプロパティエディタですので、対象となるプロパティはstring型 またはその派生型でなければなりません。
またここでは説明しませんでしたがMetaDataインターフェースを実装しているので、 追加の属性としてfilename属性を定義しています。
MetaDataインターフェースについては後述します。

4 Propertyの使い方

Propertyインターフェースはインスペクタ上で1行のプロパティに対応するクラスです。
プロパティ名とその値を保持するクラスでありStringProperty, IntegerPropertyなどの多くの実装クラスが標準で組み込まれています。

Propertyの値はgetValueまたはgetValueAsStringメソッドによって取得できます。
getValueメソッドではプロパティ値を対応するJavaのオブジェクトとして、getValueAsStringメソッドでは文字列として取得します。
値を設定するメソッドも同様にObjectを引数とするsetValueメソッドとStringを引数とするsetValueAsStringメソッドがあります。

以下に主なプロパティ型と対応するJavaオブジェクトを示します。

プロパティ型(定義ファイルでのtype属性) 対応するJavaクラス
string java.lang.String
int java.lang.Long
boolean java.lang.Boolean
choice java.lang.String
editableChoice java.lang.String
datetime java.util.Date
remoteFile java.lang.String

上記表からもわかるとおりプロパティの多くは値をString型で扱っています。
「choice」「remoteFile」などは専用のプロパティ型として定義されていますが、実質的にはStringPropertyのプロパティエディタを差し替えているだけです。

4.1 setterメソッドとUndo

setValue/setValueAsStringメソッドはその返り値としてUndoableEditを返します。
これにはデザイナーがUndo/Redoを行うための情報が含まれており、プラグイン作成時には適切にデザイナーに引き渡されなければなりません。
それぞれのプラグイン作成時にどのようにUndo情報を扱うべきかは個別のプラグインの章で説明します。

5 プロパティリスナーの作成

プロパティリスナーを作成するためにはPropertyChangeListenerインターフェース を実装したクラスを作成します。
(ここでいうPropertyChangeListenerインターフェースはJavaBeansのPropertyChangeListenerではなく、 弊社独自のクラスです。)

このリスナーでは

の2つのイベントをハンドルすることができます。

propertyChangingではPropertyChangeVetoExceptionをthrowすることで、 プロパティ値の設定をキャンセルすることもできますので、 例えばプロパティ値の設定前に値をチェックして不正な場合はエラーとするようなプロパティリスナーを 作成することができます。

5.1 実装例

sampleフォルダに正規表現でプロパティ値をチェックして、 値が不正な場合にはエラーメッセージを表示した上で設定をキャンセルする サンプル(RegexCheck.java)があります。
以下にその完全なコードを示しポイントを説明します。

package com.infoteria.asteria.flowbuilder2.plugin;

import com.infoteria.gui.property.event.PropertyChangeEvent;
import com.infoteria.gui.property.event.PropertyChangeListener;
import com.infoteria.gui.property.event.PropertyChangeVetoException;
import com.infoteria.gui.util.MetaData;
import org.w3c.dom.Element;

/**
 * Property値の変更前に正規表現によるチェックを行うPropertyListenerです。
 */
public class RegexCheck implements PropertyChangeListener, MetaData, Cloneable {
	
	private static final String A_REGEX = "regex";
	
	private String _regex;
	
	//MetaData                                                 //MetaDataインターフェースについては次章で説明します。
	public void setup(Element el) {
		_regex = el.getAttribute(A_REGEX);
	}
	
	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}
	
	//PropertyChangeListener
	public void propertyChanged(PropertyChangeEvent e) {       //1
	}
	
	public void propertyChanging(PropertyChangeEvent e) throws PropertyChangeVetoException {
		Object o = e.getNewValue();                            //2
		if (o instanceof String) {
			String str = (String)o;
			if (str.length() == 0)
				return;
			if (!str.matches(_regex)) {
				PluginUtil.showError("Invalid value: " + str); //3
				throw new PropertyChangeVetoException();       //4
			}
		}
	}
	
}
  1. このプラグインで行っているのはプロパティに設定されようとしている値を検査して、不正な値であれば設定をキャンセルすることです。
    そのためプロパティ値設定直前に発生するイベントであるpropertyChangingのみをハンドルして、 プロパティ値設定後に発生するpropertyChangedイベントでは何もしていません。
  2. 新しく設定された値を取得するにはPropertyChangeEvent#getNewValueメソッドを使用します。
    変更前の値を取得するにはPropertyChangeEvent#getOldValueメソッドを使用します。
  3. エラーダイアログを表示する場合はPluginUtil#showErrorメソッドを使用します。
    PluginUtilにはこの他に情報ダイアログや確認ダイアログを表示するためのメソッドも用意されています。
  4. プロパティ値の設定をキャンセルする場合にはPropertyChangeVetoExceptionをthrowします。
    この場合プロパティ値の設定は行われずpropertyChangedイベントも発生しなくなります。

5.2 定義ファイルへの組み込み

作成したプロパティリスナーを定義ファイルに組み込むにはPropertyListener要素を追加し、 class属性でクラス名を、target属性で対象となるプロパティ名で指定します。


<!-- メールアドレス形式のチェック -->
<PropertyListener class="com.infoteria.asteria.flowbuilder2.plugin.RegexCheck" 
    target="From" regex="[a-zA-Z0-9_\.\-]+@[A-Za-z0-9_\.\-]+"/>

RegexCheckはMetaDataインターフェースを実装しているので、 追加の属性としてregex属性を定義しています。
MetaDataインターフェースについては次章で説明します。

6 MetaDataの実装

PropertyEditorやPropertyChangeListenerを実装したクラスがさらにMetaDataインターフェースを 実装していた場合、そのクラスでは追加の定義情報を定義ファイルから読み込めるようになります。

定義ファイルの解析時にそれが定義された要素を引数としてMetaDataインターフェースのsetupメソッド が実行されるのでその情報を読み出すことができるのです。

ここまでに説明したふたつのサンプルではいずれもMetaDataインターフェースを実装し、 追加の定義情報を要素の属性値から取得していました。
RegexCheckでは値のチェックに使用する正規表現を定義ファイルの「regex」属性から取得しています。
サンプルには現れませんでしたが、要素に子要素を定義してさらに複雑な構造体を定義情報として 持たせることも可能です。

MetaDataインターフェースではもうひとつcloneメソッドも実装する必要がありますが、 プラグインではほとんどの場合DeepCopyを行う必要がないので単純にCloneableを宣言するだけで大丈夫です。

この先で説明するコンポーネントエディタと外部アプリケーション起動を作成する場合は 基底となるクラスでMetaDataインターフェースが実装されているので、 setupメソッドをオーバーライドする場合はその先頭で「super.setup(el);」を実行しなければなりません。

7 ストリーム情報の編集

プロパティの編集と同時にストリーム情報を編集するためにはまず編集対象のストリーム定義(StreamDefinition)を 取得しなければなりません。
デザイナー上ではストリーム定義はコンポーネント定義の内容に応じてコネクタに関連付けられています。

プロパティリスナーからストリーム定義を取得するためのコードは以下のようになります。

public void propertyChanged(PropertyChangeEvent e) {
    //Property#getOwnerではそのプロパティのオーナーであるコンポーネント(またはマッパー関数)が取得できます。
    Component c = (Component)e.getProperty().getOwner();
    //出力コネクタからストリーム定義を取得する場合
    //分岐がある場合はOutputConnectorSetは複数存在し、サブコネクタはConnectorSetのgetSubConnectorメソッドで取得できます。
    ComponentOutputConnector ocon = (ComponentOutputConnector)c.getOutputConnectorSet(0).getDefaultConnector();
    StreamDefinition outSd = ocon.getStreamDefinition();
    //入力コネクタからストリーム定義を取得する場合
    ComponentInputConnector icon = (ComponentInputConnector)c.getInputConnectorSet().getDefaultConnector();
    StreamDefinition inSd = icon.getStreamDefinition();
}

コンポーネントとコネクターの関係についての考え方はコンポーネント開発者ガイドのコンポーネントの構成要素の章を参考にしてください。
操作対象のストリームがデフォルトコネクタ(最初の入出力コネクタ)のものである場合はコネクタの取得にはComponentクラスの getDefaultOutputConnector(またはgetDefaultInputConnector)メソッドを使用することもできます。

コンポーネント定義によって入力ストリームをそのまま出力する、または別のコネクタを参照している場合でもComponentOutputConnector#getStreamDefinitionメソッド ではリンクをたどってそこから出力されるストリームのStreamDefinitionが取得されます。

7.1 実装例

以下にプロパティ変更に連動して出力ストリームのフィールド定義を変更するサンプルを示します。

import com.infoteria.asteria.flowbuilder2.component.Component;
import com.infoteria.asteria.flowbuilder2.component.ComponentOutputConnector;
import com.infoteria.asteria.flowbuilder2.plugin.PluginUtil;
import com.infoteria.asteria.flowbuilder2.stream.StreamDefinition;
import com.infoteria.asteria.flowbuilder2.stream.field.Field;
import com.infoteria.asteria.flowbuilder2.stream.field.FieldDefinition;
import com.infoteria.gui.property.event.PropertyChangeEvent;
import com.infoteria.gui.property.event.PropertyChangeListener;
import com.infoteria.gui.property.event.PropertyChangeVetoException;
import java.util.ArrayList;
import java.util.List;

/**
 * プロパティ変更に連動してストリームのフィールド定義を変更します。
 */
public class SetField implements PropertyChangeListener {
    
    public void propertyChanged(PropertyChangeEvent e) {
        //1 ロード中はスキップ
        Component c = (Component)e.getProperty().getOwner();
        if (c.isLoading())
            return;
        //2 Undo中はスキップ
        if (PluginUtil.inUndoProcess())
            return;
        
        List fieldNames = new ArrayList();
        String value = (String)e.getNewValue();
        if ("Pattern1".equals(value)) {
            fieldNames.add("aaa");
            fieldNames.add("bbb");
            fieldNames.add("ccc");
        } else if ("Pattern2".equals(value)) {
            fieldNames.add("field1");
            fieldNames.add("field2");
            fieldNames.add("field3");
            fieldNames.add("field4");
            fieldNames.add("field5");
        }
        if (fieldNames.size() > 0)
            doFieldChange(e, fieldNames);
    }
    
    public void propertyChanging(PropertyChangeEvent e) {
    }
    
    private void doFieldChange(PropertyChangeEvent e, List fieldNames) {
        FieldDefinition fd = new FieldDefinition();                                                           //3 新規フィールド定義作成
        for (int i=0; i<fieldNames.size(); i++) {
            String name = (String)fieldNames.get(i);
            Field f = fd.createField(name);                                                                   //4 フィールドの作成
            fd.add(f);
        }
        Component c = (Component)e.getProperty().getOwner();
        StreamDefinition sd = ((ComponentOutputConnector)c.getDefaultOutputConnector()).getStreamDefinition();//5 ストリーム定義取得
        FieldDefinition orgFd = sd.getFieldDefinition();
        e.addUndo(orgFd.importFieldDefinition(fd, true));                                                     //6 フィールド定義の差し替え
    }
}
  1. プロパティ変更のイベントはxfpファイルのロード中にも発生します。
    ロード中にはイベントをスキップしたい場合はBaseObject#isLoadingメソッドをチェックします。
  2. プロパティ変更のイベントはUndo(Redo)中にも発生します。
    この例では6番の処理でフィールド定義の差し替えをUndoに追加している、つまりの2つの処理が1回のUndo単位としてまとめられています。
    Undo時にはこの二つの処理が逆順に再生されるわけですが、この時に再びイベント処理が動いてフィールド定義の差し替えを行うと不整合が発生します。
    このためイベント内でUndo可能な処理を行った場合は必ずUndo中かどうかをチェックしてUndo中であれば処理をスキップしなければいけません。
    Undo中かどうかのチェックはPluginUtil#inUndoProcessで行います。
  3. 新しいフィールド定義を作成しています。
    Record型やCSV型のストリーム定義の場合はFieldDefinitionクラスを使用しますが、FixedLengthやXMLを扱う場合はそれぞれ、 FixedLengthFieldDefinition, XMLFieldDefinitionクラスを使用します。
  4. フィールド定義内の各フィールドの作成はFieldDefinition#createFieldメソッドで行います。
    フィールドのデータ型がString型以外の場合は作成したFieldに対してsetTypeでデータ型を設定します。
    (またはField#createFieldの名前とデータ型を引数にする版のメソッドを使用します。)
    FixedLengthやXMLの場合はFieldをそれぞれFieldFixedLength, FieldXMLにキャストすることによって追加の属性 (FixedLengthの開始位置、長さやXMLのノード型、繰り返しなど)を設定できます。
  5. コンポーネントのコネクターからストリーム定義を取得しています。
    この例ではデフォルトの出力コネクタにストリーム定義を取得しています。(大半のコンポーネントはデフォルト出力コネクタでストリームを定義します。)
  6. 元のフィールド定義に対して新しいフィールド定義をインポートしています。
    直接元のフィールド定義に対してaddやremove、FieldのsetXXXXメソッドを使用して変更を加えていないのは、そのやり方ではUndoを適切にハンドルすることが難しいからです。
    importFieldDefinitionではフィールド定義の差し替えが一つのUndoableEditにまとめられて返ってくるのでそれをPropertyChangeEvent#addUndoに渡すだけで適切にUndoが処理されます。

フィールド定義の一部のみを変更する場合は個別にadd, removeなどのメソッドを実行してそれぞれの返り値であるUndoableEditをすべてaddUndoしても構いません。
あるいはFieldDefinition#cloneで複製を作成し、複製に対して行った変更をオリジナルにインポートするという方法もあります。

7.2 XMLFieldDefinition作成時の注意

XMLフィールド定義の作成では文書要素以下のフィールドを上から順番に追加していくことになります。
コードイメージは以下のようになります。

//Root
//  Record[]
//    @id
//    Field1
//    Field2
//というフィールド定義を作成するコード


XMLFieldDefinition xfd = new XMLFieldDefinition();
//Root要素の追加
FieldXML root = (FieldXML)xfd.createFiled("Root");
root.setDepth(0);//文書要素の深さは0
xfd.add(root);

//Record要素の追加
FieldXML fRecord = (FieldXML)xfd.createField("Record");
fRecord.setDepth(1);//文書要素の子なので深さは1
fRecord.setRepeat(true);//繰り返しあり
xfd.add(fRecord);

//id属性の追加
FieldXML fId = (FieldXML)xfd.createField("id", FieldType.INTEGER);
fId.setNodeType(Node.NODE_ATTRIBUTE);//DOMのノードタイプ
fId.setDepth(2);//属性の深さは対象要素の+1
xfd.add(fId);

//Field1, Field2の追加
for (int i=0; i<2; i++) {
    FieldXML fField = (FieldXML)xfd.createField("Field" + Integer.toString(i+1));
    fField.setDepth(2);//Record要素の子なので深さは+1
    xfd.add(fRecord);
}
//作成したXML定義が正しいかどうかをチェック
try {
    xfd.validate();
} catch (Exception e) {
    //不正がある場合はExceptionが発生するのでダイアログを表示するなどのエラー処理を行う
    PluginUtil.showError(e);
}
...

上記のようにそれぞれのノードの深さや繰り返し設定を個別に設定していく必要がありますが、個別のsetメソッドやXMLFieldDefinitionへの追加時には それが完成したXMLのフィールド定義として正当かどうかはチェックされません。
(例えば文書要素の深さが1以上の数であったり、ATTRIBUTE_NODEに繰り返しが設定されていてもエラーになりません。)

不正なXML定義は後続の処理のいずれかのタイミングでエラーになりますが、基本的には作成者が不正なXML定義を作成しないように注意しておくべきです。
FieldDefinition#validateメソッドを実行すれば定義内容に不正がある場合にはエラーが発生しますので、フィールド定義作成後はこのメソッドを呼び出して、 不正定義になっていないかどうかをチェックするようにしてください。

7.3 フィールド定義作成の別法

FieldDefinition#assignTo(Element)メソッドではxfpに保存されているのと同じ形式でフィールド定義の内容を引数の要素に書き出します。
逆にFieldDefinition#assign(Element)メソッドは引数の要素に設定されている定義内容を自身に反映します。

つまりこれらのメソッドを使用すればフィールド定義の内容を外部ファイルに書き出したり、外部ファイルから読み込んだりすることができます。
コードでひとつずつ設定内容を構築していく方法でもassignメソッドによる設定でも同じことができますが、assignメソッドではUndoableEditを返さないので、 こちらを使う場合は一度別のFieldDefinitionで読み込んでからimportFieldDefinitionメソッドで内容をコピーする必要があります。

//プラグイン内のコードではほとんどの場合Undo可能なように編集操作ではUndoableEditを取得する必要がある

FieldDefinition orgFd;//更新対象のFieldDefinition
Element elFieldDef;//フィールド定義が設定されたDOMの要素

FieldDefinition temp = new FieldDefinition();
temp.assign(elFieldDef);//定義内容を読み込み。assignメソッドの返り値はnullなのでこの方法ではUndoableEditは得られない。

UndoableEdit undo = orgFd.importFieldDefinition(temp);//定義内容の読み込みとそのUndo情報の取得

7.4 プロパティリスナーでのUndoの扱い方

プロパティリスナーのコード内で編集操作(Undoが必要な操作)を行った場合は、そのUndoableEditをPropertyChangeEvent#addUndoメソッドに引き渡さなければなりません。
上記サンプルではaddUndoメソッドは一回のプロパティ変更イベント内で一度だけ実行されていますが、複数の編集操作を行ってそのすべてのUndoableEditを個別にaddUndoすることもできます。

addUndoされた編集操作はデザイナーによってプロパティ変更とまとめた一単位のUndoとして扱われるのでUndo/Redo時には行った編集操作が全て巻き戻し(あるいは再生)されます。

プロパティ変更のイベントはUndo/Redo時にも発生するのでイベント内で編集操作を行ってaddUndoした場合は必ずPluginUtil#inUndoProcessをチェックしてUndo中はイベントをスキップさせなければなりません。

8 コンポーネントエディタの作成

コンポーネントエディタはComponentEditorクラスのサブクラスとして作成します。
ComponentEditorは次のような処理が実装されています。

簡単に言えば「コンポーネント(マッパー関数)がダブルクリックされた時にどういう処理を行うか?」をプログラミングしたものがコンポーネントエディタです。

doActionメソッドはabstractメソッドとして定義されており、 プラグイン開発者がコンポーネントエディタの作成で実装しなければならないメソッドはこのメソッドのみです。

8.1 実装例

典型的なコンポーネントエディタの実装は以下のような処理の流れになります。

  1. コンポーネントからプロパティやストリーム定義の情報を収集
  2. ダイアログを表示してプロパティやストリーム定義の内容を編集
  3. ダイアログがOKボタンで終了したらプロパティやストリーム定義の内容を設定

sampleフォルダのfcsample以下に標準のメールコンポーネントのプロパティをダイアログ上でまとめて 設定できるようにしたコンポーネントエディタのサンプルが入っています。


このサンプルはインスペクタの内容をダイアログに配置し直しただけなので、必ずしも必要ではありませんが 配置や説明を工夫するだけでもコンポーネントで設定すべき内容がわかりやすくなりますし、 コンポーネントエディタの作成に必要な技術要素がおおむね網羅されています。

このサンプルコードには以下の処理が含まれています。

意図的に平坦な実装にしてあるので、コードの解釈はそれほど難しくないですが以下に主な技術要素について説明を加えます。

8.2 コンポーネント(マッパー関数)からの情報の取得と設定

コンポーネントエディタではコンポーネント(マッパー関数)自身の取得は、 BaseObjectUndoableEditEvent#getBaseObjectメソッドで行います。

このメソッドの返り値であるBaseObjectはComponentとFunctionの共通のスーパークラスですので、 キャストしてComponentまたはFunctionに変換することが可能です。

//BaseObjectUndoableEditからの情報取得
BaseObjectUndoableEdit e;
Component c = (Component)e.getBaseObject();
Property fromProp = c.getProperty("From");
Property toProp = c.getProperty("To");
CategoryProperty headerProp = (CategoryProperty)c.getAdditionalProperty("AdditionalHeaders");
...

最初に示したクラス図にもあるとおりBaseObjectはPropertyとAdditinalPropertyInterfaceを複数持つことができ、 getPropertyメソッドまたはgetAdditionalPropertyメソッドによってそれらを取得することができます。

値の設定を行う場合、それはUndo可能にしなければならないのでUndo情報をBaseObjectUndoableEvent#addUndoメソッド に引き渡します。
このメソッドには複数のUndo情報を個別に設定することができ、また引数がnullの場合はそれを無視するのでPropertyの setValueメソッド(またはsetValueAsStringメソッド)の返り値をそのまま渡せます。

...
e.addUndo(fromProp.setValueAsString(dlg.getFrom()));
e.addUndo(toProp.setValueAsString(dlg.getTo()));
...

8.3 CategoryPropertyの使い方

CategoryPropertyは表形式で値を設定させるプロパティです。

CategoryPropertyでは列の情報はPropertyクラスで保持しており、各列の編集方法は定義ファイルでのProperty要素 定義によって決まります。

例えばMailコンポーネントの「追加するヘッダー」の定義は以下のようになっています。

<Category displayName="追加するヘッダー" key="Name" mapping="true" name="AdditionalHeaders" value="Default">
    <Property choiceItem="Message-Id&#xa;Reply-To&#xa;In-Reply-To" displayName="ヘッダー名" name="Name" toolTip="header name" type="editableChoice"/>
    <Property displayName="初期値" name="Default" toolTip="default" type="string"/>
</Category>

この定義では1列目は「Name」という名前で直接編集も可能なドロップダウンリスト形式(editableChoice)。
2列目は「Default」という名前で単純な文字列形式です。

一方、行の情報はCategoryItemというクラスで保持しています。
CategoryItemではgetValue/setValueというメソッドで各列の値を出し入れできます。
getValueの返り値、setValueの値を示す引数はObject型ですが、そのObjectは対応する列で定義したPropertyの扱うデータクラスでなければなりません。
新しい行を追加する場合はCategoryProperty#createNewItemでCategoryItemを作成し、値を設定後にaddItemします。

//CategoryProperty行追加のコードイメージ
//ただしUndoは考慮していない
CategoryProperty prop;
CategoryItem item = prop.createNewItem();
item.setValue("Name", "X-Mailer");
item.setValue("Default", "MailClient-X");
prop.addItem(item);

CategoryItem#setValueやCategoryProperty#addItemの返り値はUndoableEditであり、個別に値を設定する場合は それらのUndoableEditを適切に処理しなければなりません。
ですが、まとめて値を設定するのであれば最初にCategoryPropertyの複製を作成し、それに対して編集操作を行った後に PluginUtil#updateCategoryPropertyメソッドで複製の内容をオリジナルに反映させた方が簡単です。

//CategoryProperty複製による編集のコードイメージ
BaseObjectUndoableEdit e;
CategoryProperty origin;
CategoryProperty copy = (CategoryProperty)origin.clone();

//ToDo copyに対する編集
...

UndoableEdit undo = PluginUtil.updateCategoryProperty(origin, copy);
e.addUndo(undo);

あるいはインスペクタで使用している表形式のUIをそのまま使用することも可能です。

CategoryProperty origin;
CategoryProperty copy = (CategoryProperty)origin.clone();
java.awt.Component editorComponet = copy.createEditorComponent();

//ToDo パネルなどにeditorComponentを貼り付けて表示
...

copy.releaseEditorComponent(editorComponent);//createEditorComponentで作成したエディタは必ず使用後にreleaseEditorComponentで解放
UndoableEdit undo = PluginUtil.updateCategoryProperty(origin, copy);
e.addUndo(undo);

CategoryProperty#createEditorComponentではインスペクタのタブページに表示される部品がそのまま取得でき、 任意のコンテナに載せて使用することができます。
ここで注意が必要なのはこの方法を使用する場合は必ず最初にCategoryPropertyを複製する必要がある点です。
エディタコンポーネントではその生成元のCategoryPropertyを直接編集するのでオリジナルに対してこれを行うとUndoが処理できなくなります。

サンプルのダイアログはSwingのGUI部品を単純に並べただけですが、実際にはここではどのような複雑な処理であっても Javaで作成可能でさえあれば作成して組み込むことが可能です。

8.4 定義ファイルへの組み込み

作成したコンポーネントエディタを定義ファイルに組み込むにはListener要素を追加し、 class属性でクラス名を、menuItem属性で右クリックメニューに追加するキャプションを指定します。


<!-- メールアドレス形式のチェック -->
<Listener class="com.infoteria.asteria.sample.plugin.MailEditor" 
    menuItem="プロパティの編集"/>

このサンプルでは使用していませんが、setupメソッドをオーバーライドして 独自の定義項目を追加することも可能です。
(setupメソッドをオーバーライドする場合は先頭でsuper.setupを行ってください。)

9 汎用コネクションとpluginCall

汎用コネクションは任意の名前と値のセットをコネクション定義として保存しておける仕組みです。
これを利用して特殊な接続方法を使用するプロダクトと連携するようなコンポーネントを作成することができます。
(汎用コネクションについての説明はコンポーネント開発者ガイドにもあります。)

pluginCallはサーバー側で実装したコードをデザイナーから呼び出して何らかの情報を返すための仕組みです。

これらを組み合わせることで例えば特定のプロダクトからAPI経由で情報を取得し、デザイナーにその情報を反映 させるようなプラグインを作成することができます。
通常プロダクトのAPIの実行のためには専用のjarファイルが必要となるため、デザイナーから直接APIを実行するように 作成した場合jarファイルの配置等のインストール作業が煩雑になりますが、この方法であればプロダクトのjarファイル はサーバー側だけに配置すれば良くなります。


9.1 サーバー側サンプル

サーバー側でのpluginCallの実装はComponentクラスのpluginCallメソッドをオーバーライドすることで行います。
サーバー側コードのサンプルを以下に示します。
このサンプルではデザイナーからのリクエストが「Tables」であるか「Columns」であるかによって、処理を振り分けテーブル一覧または列一覧を返しています。
(実際の処理としては汎用コネクションに設定された情報を元にAPIでそれらの情報を取得する想定ですが、ここではダミーの情報を返しています。)

import com.infoteria.asteria.connection.CommonConnectionEntry;
import com.infoteria.asteria.flowengine2.execute.ExecuteContext;
import com.infoteria.asteria.flowengine2.execute.TestContext;
import com.infoteria.asteria.flowengine2.flow.InputConnector;
import com.infoteria.asteria.flowengine2.soap.PluginRequest;
import com.infoteria.asteria.flowengine2.soap.PluginResponse;
import com.infoteria.asteria.flowlibrary2.component.SimpleComponent;
import com.infoteria.asteria.flowlibrary2.component.ComponentException;
import com.infoteria.asteria.flowlibrary2.FlowException;
import com.infoteria.asteria.flowlibrary2.stream.StreamType;
import com.infoteria.asteria.util.xml.DOMUtil;
import com.infoteria.asteria.value.Value;
import com.infoteria.asteria.value.VariableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * 汎用コネクションの情報を使用して
 * テーブル一覧とテーブルの列一覧を取得するサンプル
 */
public class PluginCallComponent extends SimpleComponent {
    
    public static final String COMPONENT_NAME = "PluginCallSample";
    public String getComponentName() { return COMPONENT_NAME;}
    
    public PluginCallComponent() {
        getInputConnector().setAcceptLinkCount(1);
        getInputConnector().setAcceptType(StreamType.ALL);
        getOutputConnector().setAcceptType(StreamType.ALL);
    }
    
    public boolean execute(ExecuteContext context) throws FlowException {
        //プラグインのサンプルのため実際のコンポーネント実行コードは未実装
        passStream();
        return true;
    }
    
    public PluginResponse pluginCall(TestContext context, PluginRequest request) throws FlowException {
        try {
            VariableList param = request.getParams();
            //パラメータから実行する処理を取得
            Value method = param.getValue("Method");
            if (method == null || !(method.strValue().equals("Tables") || method.strValue().equals("Columns")))
                throw new ComponentException("Unknown method: " + method);
            
            //パラメータから汎用コネクションを取得
            Value conName = param.getValue("Connection");
            if (conName == null)
                throw new ComponentException("Connection not found: " + conName);
            CommonConnectionEntry con = (CommonConnectionEntry)context.getConnectionEntry(CommonConnectionEntry.TYPE, conName.strValue());
            if (con == null)
                throw new ComponentException("Connection not found: " + conName);
            
            List list = null;
            if ("Tables".equals(method.strValue()))
                list = getTables(con);
            else {//Columns
                Value tableName = param.getValue("TableName");
                if (tableName == null)
                    throw new ComponentException("Table not found: " + tableName);
                list = getTableColumns(con, tableName.strValue());
            }
            //結果をXML形式で生成
            //※1 DOMUtilはXMLを扱うためのユーティリティクラス。DOMはJAXPのAPIを使用して作成
            //※2 名前空間がない場合でもElementの作成にはNamespaceAPIを使用する
            Document doc = DOMUtil.newInstance().createDocument();
            Element root = doc.createElementNS(null, method.strValue());
            doc.appendChild(root);
            for (int i=0; i<list.size(); i++) {
                String name = (String)list.get(i);
                Element child = doc.createElementNS(null, "Name");
                child.appendChild(doc.createTextNode(name));
                root.appendChild(child);
            }
            return new PluginResponse(doc);
        } catch (FlowException e) {
            throw e;
        } catch (Throwable e) {
            //デバッグ用
            context.error(e);
            return new PluginResponse(e.toString());
        }
    }
    
    private List getTables(CommonConnectionEntry con) throws FlowException {
        List ret = new ArrayList();
        Map map = con.getParameterMap();
        if (map == null)
            throw new ComponentException("Connection not defined");
        
        //mapに設定されているパラメータから対象プロダクトとの接続に必要な情報を取得
        String host = (String)map.get("Host");
        String strPort = (String)map.get("Port");
        
        //... ここで接続情報を元に対象プロダクトから情報を取得して返す
        //ここではダミーを返している
        ret.add("Table1");
        ret.add("Table2");
        ret.add("Table3");
        return ret;
    }
    
    private List getTableColumns(CommonConnectionEntry con, String tableName) throws FlowException {
        //ToDo getTablesと同様に接続情報を元に対象プロダクトから情報を取得して返す
        //ここではダミーを返している
        List ret = new ArrayList();
        ret.add(tableName + "_Col1");
        ret.add(tableName + "_Col2");
        ret.add(tableName + "_Col3");
        return ret;
    }
}

pluginCallのリクエストはPluginRequest、レスポンスはPluginResponseというクラスを使用して 作成します。
実際にはPluginResponseはXMLをラップしているだけですので作り方次第でどのような情報でもデザイナー側に返すことができます。

9.2 デザイナー側サンプル

デザイナー側ではPluginUtil#pluginCallメソッドでサーバー側のコードを呼び出します。
返り値のXMLをパースすることで情報を取得してダイアログを表示したり、フィールド定義を設定したりすることができます。

先のサーバー側コードを呼び出すデザイナー側のサンプルを以下に示します。
このサンプルではコンポーネントダブルクリック時にテーブル一覧を表示し、選択されたテーブルの列情報をコンポーネントの 入力ストリームに設定しています。(RDBPutライクなコンポーネントを想定しています。)

import com.infoteria.asteria.flowbuilder2.component.Component;
import com.infoteria.asteria.flowbuilder2.event.BaseObjectUndoableEvent;
import com.infoteria.asteria.flowbuilder2.plugin.ComponentEditor;
import com.infoteria.asteria.flowbuilder2.plugin.PluginUtil;
import com.infoteria.asteria.flowbuilder2.resource.UI;
import com.infoteria.asteria.flowbuilder2.stream.field.FieldDefinition;
import com.infoteria.asteria.flowengine2.soap.PluginRequest;
import com.infoteria.asteria.flowengine2.soap.PluginResponse;
import com.infoteria.asteria.util.xml.DOMUtil;
import com.infoteria.gui.property.Property;
import com.infoteria.gui.property.PropertyException;
import com.infoteria.gui.util.SpringLayoutUtil;
import com.infoteria.asteria.value.Value;
import com.infoteria.asteria.value.VariableList;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.FlowLayout;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import com.infoteria.asteria.flowbuilder2.component.ComponentInputConnector;
import com.infoteria.asteria.flowbuilder2.dialog.MessageDialog;

/**
 * コンポーネントダブルクリック時にテーブル選択のダイアログを表示し、
 * 選択したテーブルの列情報を入力のフィールド定義に設定するサンプル
 */
public class PluginCallEditor extends ComponentEditor {
    
    /**
     * コンポーネントエディタが実際に行う処理
* @param e イベントオブジェクト * @param bEditable コンポーネントが編集可能かどうか */ protected void doAction(BaseObjectUndoableEvent e, boolean bEditable) { //イベントオブジェクトから対象のコンポーネントを取得するには //BaseObjectUndoableEvent#getBaseObjectメソッドを使用する //マッパー関数の場合はこのメソッドの返り値はFunctionになる Component c = (Component)e.getBaseObject(); //Componentからのプロパティの取得はComponent#getPropertyメソッドを使用する Property connectionProp = c.getProperty("Connection"); String conName = connectionProp.getValueAsString(); if (conName == null || conName.length() == 0) { MessageDialog.showError("コネクションが指定されていません"); return; } //コンポーネントに渡すパラメータの設定 PluginRequest request = new PluginRequest("PluginCallSample"); VariableList param = new VariableList(); param.putValue("Method", new Value("Tables")); param.putValue("Connection", new Value(conName)); request.setParams(param); try { PluginResponse res = PluginUtil.pluginCall(request); List list = buildList(res.getDocument()); MyDialog dlg = new MyDialog(PluginUtil.getApplicationFrame(), "Sample", list); try { dlg.setVisible(true); if (!dlg.isOK()) return; //選択されたテーブルの列一覧をpluginCallから取得する String tableName = dlg.getTableName(); param.clear(); param.putValue("Method", new Value("Columns")); param.putValue("Connection", new Value(conName)); param.putValue("TableName", new Value(tableName)); res = PluginUtil.pluginCall(request); FieldDefinition fd = ((ComponentInputConnector)c.getDefaultInputConnector()).getStreamDefinition().getFieldDefinition(); FieldDefinition fd2 = buildFieldDefinition(buildList(res.getDocument())); e.addUndo(fd.importFieldDefinition(fd2)); } finally { dlg.dispose(); } } catch (IOException ex) { MessageDialog.showError(ex); } } /** * 文書要素以下の要素の要素内容をListにする */ private List buildList(Document doc) { List ret = new ArrayList(); Node node = doc.getDocumentElement().getFirstChild(); while (node != null) { if (node.getNodeType() == Node.ELEMENT_NODE) ret.add(DOMUtil.getChildText((Element)node)); node = node.getNextSibling(); } return ret; } /** * Listの文字列をフィールド名としてFieldDefinitionを作成する */ private FieldDefinition buildFieldDefinition(List list) { FieldDefinition fd = new FieldDefinition(); for (int i=0; i<list.size(); i++) { String name = (String)list.get(i); fd.add(fd.createField(name)); } return fd; } /** * コンボボックスでテーブルを選択させるダイアログ */ private static class MyDialog extends JDialog { private boolean _bOK = false; private JButton _btnOK = new JButton(UI.OK); private JButton _btnCancel = new JButton(UI.CANCEL); private JComboBox _cmbTables; public MyDialog(JFrame owner, String title, List list) { super(owner, title, true); JPanel main = new JPanel(new FlowLayout()); main.add(new JLabel("テーブル名: ")); _cmbTables = new JComboBox(list.toArray()); Dimension d = _cmbTables.getPreferredSize(); d.width += 20; _cmbTables.setPreferredSize(d); main.add(_cmbTables); getContentPane().add(main); getContentPane().add(createFooterPanel(), BorderLayout.SOUTH); pack(); setLocationRelativeTo(owner); } private JPanel createFooterPanel() { Action a = new AbstractAction() { public void actionPerformed(ActionEvent e) { _bOK = e.getSource() == _btnOK; setVisible(false); } }; _btnOK.addActionListener(a); _btnCancel.addActionListener(a); JPanel panel = new JPanel(new FlowLayout()); panel.add(_btnOK); panel.add(_btnCancel); getRootPane().setDefaultButton(_btnOK); InputMap imap = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Close"); ActionMap amap = getRootPane().getActionMap(); amap.put("Close", a); return panel; } public void setEditable(boolean b) { _btnOK.setEnabled(b); } public boolean isEditable() { return _btnOK.isEnabled(); } public boolean isOK() { return _bOK;} public String getTableName() { return (String)_cmbTables.getSelectedItem(); } } }

上の例ではサーバー側では独自にループを回してXMLを作成していますが、単純に文字列のリストを返すだけならString[]を引数に取る PluginResponseのコンストラクタを使うこともできます。
このコンストラクタでは内部的に「ItemList」を文書要素とし、「Item」を文字列のホルダ要素とするXMLを作成します。
PluginResponseがこの形式でXMLを返した場合、その文字列のリストはデザイナー側でPluginCallPropertyを定義することによって、 インスペクタ上でのドロップダウンリスト形式のプロパティとすることができます。

10 標準プラグインの自作コンポーネントへの組み込み

プラグインの作成ではすべての機能を自分で作成することもできますが、標準で提供されている部品を組み込むこともできます。
現在は以下の二つの部品のプラグインへの組み込み方法が公開されています。

いずれもRDB関連の補助UIクラスです。
これらのクラスはSQLToolというユーティリティクラスを介して使用することができます。

10.1 SQLBuilder

SQLBuilderはRDBGetコンポーネントで使用しているSELECT文を組み立てるためのUIです。
実際にはサーバー側のRDBGetコンポーネントが必要とするのはSQLのみなのでGUIでSELECT文を組み立てるという要件は必須ではありませんが、 これがあることによってユーザーはSQL文とそのフィールド定義を手動で行う煩わしさから解放されます。

10.1.1 SQLBuilderを使用するコンポーネントのプロパティ

SQLBuilderを起動するために必要な情報としては以下の二つがあります。

これらの情報は通常はコンポーネントのプロパティから取得します。
またSQLBuilderの実行結果から取得できる情報としては以下の3つがあります。

これらの情報は通常はSQLBuilderの終了時(OKボタンで終了した時)にプロパティまたはストリームのフィールド定義に設定するようにプログラミングします。

これらのプロパティを定義した定義ファイルは以下のようになります。


<!-- RDBGetの定義ファイルより抜粋 -->
<Property connection="RDBConnection" displayName="コネクション名" required="true" mapping="false" name="Connection" toolTip="Connection" type="connection"/>
<Property displayName="SQL文" editor="multiline" name="SQL" required="true" toolTip="SQL" type="string"/>
<Property class="com.infoteria.asteria.flowbuilder2.sqlbuilder.SQLBuilderProperty" displayName="SQL Builder" name="SQLBuilder" toolTip="SQLBuilder" visible="false"/>
<Category key="Name" mapping="true" name="SQLParameter" displayName="SQLパラメータ" readonly="true" value="Default">
	<Property displayName="パラメータ名" name="Name" toolTip="header name" type="string"/>
	<Property displayName="データ型" name="Type" toolTip="data type" type="choice">
		<Value>String</Value>
		<ChoiceItem ref="DataType"/>
	</Property>
	<Property displayName="値" name="Default" toolTip="header value" type="string"/>
</Category>

SQLBuilderのモデルの保存用にSQLBuilderという名前の専用プロパティを使用しています。
(デザイナーにプロパティ型として登録されていないためtypeではなくclassでプロパティを直接指定します。)
SQLBuilderを使用するコンポーネントが必ずこのセットのプロパティを持たなければならないわけではありませんが、 ほとんどの場合このようなプロパティ構成となります。

10.1.2 SQLBuilderの使用例

sampleディレクトリにある「SQLBuilderSample」を参照してください。
処理の流れとしては

  1. プロパティから必要な情報を取得
  2. SQLBuilderDialogを作成して現在の値を設定
  3. SQLBuilderDialogがOKボタンで終了した場合はプロパティに値を設定

となります。
値の設定にはSQLToolのユーティリティメソッドが使用できます。
ほとんどの場合SQLBuilderを組み込むプラグインのコードはこのサンプルそのままになります。

10.1.3 SQL文のパース

SQLBuilderで生成されるSQL文には文字列中に「?param1?」「$param2$」のような独自のパラメータが埋め込まれています。
このSQL文はUtilityクラスのparseSQLメソッドを通すことによってJavaのPreparedStatementで実行可能な形式に変換されます。
具体的には以下の変換がおこなわれます。

SQL文を取得した後はPreparedStatementなどのJDBCのAPIを用いて処理を行ってください。

10.2 テーブル選択ダイアログ

テーブル選択ダイアログはRDBPutコンポーネントで使用しているテーブルとその列を選択するためのUIです。
選択した列情報をフィールド定義やCategoryPropertyに設定することでフィールド定義を簡略化することができます。

10.2.1 テーブル選択ダイアログを使用するコンポーネントのプロパティ

テーブル選択ダイアログを起動するためには「テーブル情報を取得するためのコネクション」の情報が必要です。
また通常は選択したテーブル名を設定するためのプロパティ、選択した列を設定するためのフィールド定義(またはCategoryProperty)も 必要になります。

これらのプロパティを定義した定義ファイルは以下のようになります。


<!-- RDBPutの定義ファイルより抜粋 -->
<Property connection="RDBConnection" displayName="コネクション名" required="true" mapping="false" name="Connection" toolTip="Connection" type="connection"/>
<Property displayName="テーブル名" class="com.infoteria.asteria.flowbuilder2.sqlbuilder.TableSelectPropertyEditor" required="true" name="TableName" mapping="true" toolTip="TableName" type="string"/>
<Category displayName="入力" key="Name" mapping="false" name="Field(Input)" >
	<Property displayName="フィールド名" name="Name" readonly="true" toolTip="header name" type="string"/>
	<Property displayName="データ型" name="Type" readonly="true" toolTip="data type" type="choice">
		<Value>String</Value>
		<ChoiceItem ref="DataType"/>
	</Property>
	<Property displayName="キーにする" name="Key" toolTip="Key" type="boolean">false</Property>
</Category>
<Input accept="Record" defineStream="true">
	<FieldDef readonly="true"/>
</Input>

RDBPutでは列情報はCategoryPropertyと入力ストリームのフィールド定義の両方に設定しています。
フィールド定義はテーブル選択ダイアログでしか設定できないようにするためreadonlyとして定義しています。
CategoryPropertyの方ではフィールド名とデータ型は変更できず、「キーにする」列のみ変更可能なように定義されています。

列情報をどこに設定するかはコンポーネントの種類によって変わってきますがほとんどの場合テーブル選択ダイアログ を使用するコンポーネントではこのようなプロパティ構成となります。

10.2.2 テーブル選択ダイアログの使用例

sampleディレクトリにある「TableSelectSample」を参照してください。
処理の流れとしては

  1. プロパティから必要な情報を取得
  2. TableSelectDialogを作成して現在の値を設定
  3. TableSelectDialogがOKボタンで終了した場合はプロパティに値を設定

となります。
値の設定にはSQLToolのユーティリティメソッドが使用できます。
列情報をどこに設定するかはコンポーネントの種類によって変わってきますがほとんどの場合テーブル選択ダイアログ を組み込むプラグインのコードはこのサンプルのようなコードになります。

11 外部アプリケーション起動の作成

外部アプリケーションを起動してその情報をデザイナーに取り込む場合ははLaunchExternalAppを使用します。
LaunchExternalAppはComponentEditorとは違いabstractクラスではないのでサブクラスを作成せずにそのまま使用することも可能です。
例えば次の定義は文字列型のプロパティ値をWindowsのメモ帳で編集し、その結果をプロパティ値に設定します。


<Listener class="com.infoteria.asteria.flowbuilder2.plugin.LaunchExternalApp" menuItem="ソースの編集" 
    command="notepad.exe Temp.java" appName="Javaソース編集" execDir="toolshedDir">
	<SaveProperty name="Source" filename="Temp.java"/>
</Listener>

LaunchExternalAppにはpreExecute/postExecuteというメソッドがあり外部アプリケーションの起動前後に実行されます。
デフォルトではpreExecute/postExecuteは以下の処理を行います。

preExecute 定義ファイルにSaveProperty要素で指定されたプロパティ値を指定のファイルに保存
postExecute preExecuteで書き出したファイルが更新されていたらその内容をプロパティに書き戻す

つまり上記定義の場合の実際の処理の流れは以下のようになります。

  1. 「Source」プロパティの設定値を「Temp.java」というファイルに保存
  2. notepad.exeを起動してその終了を監視
  3. notepad.exe終了時に「Temp.java」が更新されていればその内容を「Source」プロパティに設定

外部アプリケーションの起動中は以下のようなダイアログが表示され、デザイナーは外部アプリケーションが 終了するまでロックします。


サブクラスを作成する場合、preExecute/postExecuteをオーバーライドして処理を行います。
(例えばCategoryPropertyやFieldDefinitionを読み書きするなどの処理が行えます。)
プロパティ値の取得や設定の方法自体はコンポーネントエディタの場合と同じです。

便宜上ここでは説明項目を分けていますが、実際には外部アプリケーション起動はコンポーネントエディタの一種です。
つまりコンポーネントエディタのdoActionメソッドの実装が以下の処理になっているものが外部アプリケーション起動です。

  1. preExecuteメソッドの実行
  2. 外部アプリケーションの起動
  3. postExecuteメソッドの実行

外部アプリケーション起動のサンプルは現在ありませんが興味のある方は御相談ください。