プラグインとはデザイナーの動作を拡張し、コンポーネントやマッパー関数のプロパティ設定などの補助をするモジュールのことです。
コンポーネントやマッパー関数のクラスがフローの実行時にサーバー側で動作するモジュールであるのに対し、
プラグインはフローの設計時にデザイナー側で動作するのでフローの実行には影響を与えません。
プラグインはコンポーネント/マッパー関数の作成で必ず作らなければならないというものではありませんが、
多機能なコンポーネント/マッパー関数を作成する場合プラグインを作成するとデザイナーの操作性が向上し、
フロー作成の効率が大幅にアップすることもあります。
プラグイン作成では多くのクラスを使用します。
それらのクラス群はサーバーで使用しているクラスと同一の場合もありますが、ほとんどの場合プラグインで使用する
クラスとサーバーで使用するクラスは全く別物です。
たとえばプラグインでも「Component」というクラスを使用しますが、そのパッケージは
com.infoteria.asteria.flowbuilder2.component.Component
であり、サーバーで使用するクラス
com.infoteria.asteria.flowengine2.flow.Component
とは全く別のクラスです。
後者はサーバー上で実際にコンポーネントの行う処理を実行するクラスですが、
前者はデザイナーで使用されるxfpに保存されているコンポーネントの内容をラップしたクラスです。
プラグインの開発はJDK8.0以降の環境で行ってください。
また、「DESIGNER_HOME/lib」にある以下のjarファイルにクラスパスを通してください。
※ascore-1610.0200.jarと asdesigner-1610.0200.jarの「-1610.0200」の部分はインストールしたバージョンにより異なります。
ほとんどの場合プラグインはそれを使用するサーバー側のクラスと同時に開発され、同じjarファイルに含められます。
そのため実際にはコンポーネント(またはマッパー関数)の作成に必要なjarファイルもクラスパスに含めることになります。
作成するプラグインの内容によってはさらに別のjarファイルがコンパイル/実行に必要になる場合があります。
その場合、flow-ctrlのshowlibコマンドを使用することで必要とするクラスがどのjarに含まれているかを確認することができます。
//showlibコマンドによって指定のクラスがどのjarに含まれているかが表示されます。 >showlib com.infoteria.asteria.flowlibrary2.stream.StreamDataXML jar:file:C:\Program Files\asteria5\server\lib\ascore-1610.0200.jar!/com/infoteria/asteria/flowlibrary2/stream/StreamDataXML.class
必要に応じてここで表示されたjarファイルにもクラスパスを通してください。
実際にはウィザードで生成されるANT用のbuild.xmlでは「DESIGNER_HOME/lib/**/*.jar」にクラスパスが通っているので、通常はbuild.xmlの設定を変更する必要はありません。
プラグイン作成時にコード中に埋めた「System.out.println」やExceptionのStackTraceはデザイナーのerror.logに出力されます。
デザイナーのerror.logはデザイナーのメニューの「エラーログの表示」から表示させることができます。
現在作成可能なプラグインの種類は次の4つです。
以下にそれぞれのプラグインの概要を説明します。
コンポーネントやマッパー関数がどのようなプロパティを持っているかは、 定義ファイルのProperty要素で定義されます。
プロパティの型は定義ファイル内でtype属性によって示されます。
int型のプロパティの場合は数値しか入力できす、
choice型のプロパティではドロップダウンリストからプロパティ値が選択できるようになっています。
またdate型やdatetime型のプロパティのように日付を選択するカレンダーが表示されるものもあります。
つまりプロパティの型によって値を設定するためのUIが決まっています。
そのプロパティ値を設定するためのUIがプロパティエディタです。
通常プロパティ型とプロパティエディタは1対1で対応していますが、 string型のようにeditor属性でプロパティエディタを選択できるものもあります。
editor属性値 | プロパティエディタ |
---|---|
デフォルト(省略時) | 1行テキストダイアログ |
multiline | 複数行テキストダイアログ |
condition | 条件式の補完機能つき1行テキストダイアログ |
もっともわかりやすい例はBranchStartコンポーネントの条件式プロパティで使用されている
「condition」エディタでしょう。
このエディタは「$」や「.」をタイプした時に条件式の補完をしてくれます。
このようにプロパティ値を設定するための補助的な機能を付加したUIを自作できるプラグインが
プロパティエディタです。
プロパティリスナーはJavaプログラムで日常的に使用されるEventListenerの一種で、 プロパティ値の変更に反応するリスナーです。
プロパティ値の設定前後でイベントが発生するのでそこで何かしらの処理を行うことができます。
PropertyListenerの典型的な実装例としては、プロパティ値の変更に伴い別のプロパティの表示状態など をコントロールできるSimplePropertyControllerがあります。
コンポーネントエディタとは複数のプロパティ値やフィールド定義などをまとめて設定できるように
したものです。
コンポーネントエディタを使用している例としては
などがあります。
外部アプリケーション起動はその名のとおり外部アプリケーションを起動するためのプラグインです。
外部アプリケーション起動での処理の流れは以下のようになります。
外部アプリケーション起動を使用した例としてはExcelBuilder、PDFBuilderなどがあります。
プラグイン作成に関連するクラスの簡略化したクラス図を以下に示します。
青色で表示されているのが開発可能なプラグインのインターフェースまたはベースクラスです。
緑色は標準で組み込まれているプラグイン具象クラスの一部です。
これらの中には定義ファイルに定義するだけでそのまま使用可能なものもあります。
(詳細は定義ファイルリファレンスをご覧ください。)
上の図に示されている事実を以下に簡単に説明します。
サーバー側の開発ではComponentやFunctionを継承して「FileGet」や「RDBPut」といった具体的な処理を行うクラスを作成するわけですが、 デザイナー上ではComponentやFunctionは末端の具象クラスであり、それ以上のサブクラスは基本的にありません。
といったコンポーネント固有の動作はすべてプラグインによって実現されています。
標準プラグインを定義ファイルに組み込むだけでも多くのことができますが、独自のプラグインを作成することでより細やかな設定が可能になります。
プロパティエディタを作成するためにはPropertyEditorインターフェース
を実装したクラスを作成します。
このインターフェースはSwingのTableCellEditorインターフェースを拡張し、setPropertyメソッドを
追加したものです。
新しいPropertyEditorを作成する場合にインターフェースのメソッドをすべて自分で作成しなければ ならないケースはほとんどなく、通常は次のButtonTextCellEditorかButtonCellEditorを継承して作成します。
ButtonTextCellEditorはインスペクタの編集エリア内にテキストフィールドがあり、
その脇に「...」という小さなボタンがあるタイプのプロパティエディタです。
ボタンをクリックすることでそのプロパティ値を設定するための補助UI(ほとんどの場合はダイアログ)
が起動します。
ButtonTextCellEditorを継承したPropertyEditorの例としては
などがあります。
ButtonTextCellEditorを作成する場合実装しなければならないメソッドはdoButtonActionメソッド のみであり、そこにボタンがクリックされた場合の処理を記述します。
ButtonCellEditorはインスペクタの編集エリア全体がボタンで覆われたタイプのプロパティエディタです。
つまりインスペクタ内で値を設定することはできず、編集エリアをクリックすることでそのプロパティ値を設定するための補助UI(ほとんどの場合はダイアログ)
が起動します。
ButtonCellEditorを継承したPropertyEditorの例としては
などがあります。
ButtonCellEditorを作成する場合実装しなければならないメソッドはdoButtonActionメソッド のみであり、そこにボタンがクリックされた場合の処理を記述します。
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(); } }
作成したプロパティエディタを定義ファイルに組み込むには対象の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インターフェースについては後述します。
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のプロパティエディタを差し替えているだけです。
setValue/setValueAsStringメソッドはその返り値としてUndoableEditを返します。
これにはデザイナーがUndo/Redoを行うための情報が含まれており、プラグイン作成時には適切にデザイナーに引き渡されなければなりません。
それぞれのプラグイン作成時にどのようにUndo情報を扱うべきかは個別のプラグインの章で説明します。
プロパティリスナーを作成するためにはPropertyChangeListenerインターフェース
を実装したクラスを作成します。
(ここでいうPropertyChangeListenerインターフェースはJavaBeansのPropertyChangeListenerではなく、
弊社独自のクラスです。)
このリスナーでは
の2つのイベントをハンドルすることができます。
propertyChangingではPropertyChangeVetoExceptionをthrowすることで、 プロパティ値の設定をキャンセルすることもできますので、 例えばプロパティ値の設定前に値をチェックして不正な場合はエラーとするようなプロパティリスナーを 作成することができます。
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 } } } }
作成したプロパティリスナーを定義ファイルに組み込むには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インターフェースについては次章で説明します。
PropertyEditorやPropertyChangeListenerを実装したクラスがさらにMetaDataインターフェースを 実装していた場合、そのクラスでは追加の定義情報を定義ファイルから読み込めるようになります。
定義ファイルの解析時にそれが定義された要素を引数としてMetaDataインターフェースのsetupメソッド が実行されるのでその情報を読み出すことができるのです。
ここまでに説明したふたつのサンプルではいずれもMetaDataインターフェースを実装し、
追加の定義情報を要素の属性値から取得していました。
RegexCheckでは値のチェックに使用する正規表現を定義ファイルの「regex」属性から取得しています。
サンプルには現れませんでしたが、要素に子要素を定義してさらに複雑な構造体を定義情報として
持たせることも可能です。
MetaDataインターフェースではもうひとつcloneメソッドも実装する必要がありますが、 プラグインではほとんどの場合DeepCopyを行う必要がないので単純にCloneableを宣言するだけで大丈夫です。
この先で説明するコンポーネントエディタと外部アプリケーション起動を作成する場合は 基底となるクラスでMetaDataインターフェースが実装されているので、 setupメソッドをオーバーライドする場合はその先頭で「super.setup(el);」を実行しなければなりません。
プロパティの編集と同時にストリーム情報を編集するためにはまず編集対象のストリーム定義(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が取得されます。
以下にプロパティ変更に連動して出力ストリームのフィールド定義を変更するサンプルを示します。
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 フィールド定義の差し替え } }
フィールド定義の一部のみを変更する場合は個別にadd, removeなどのメソッドを実行してそれぞれの返り値であるUndoableEditをすべてaddUndoしても構いません。
あるいはFieldDefinition#cloneで複製を作成し、複製に対して行った変更をオリジナルにインポートするという方法もあります。
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メソッドを実行すれば定義内容に不正がある場合にはエラーが発生しますので、フィールド定義作成後はこのメソッドを呼び出して、
不正定義になっていないかどうかをチェックするようにしてください。
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情報の取得
プロパティリスナーのコード内で編集操作(Undoが必要な操作)を行った場合は、そのUndoableEditをPropertyChangeEvent#addUndoメソッドに引き渡さなければなりません。
上記サンプルではaddUndoメソッドは一回のプロパティ変更イベント内で一度だけ実行されていますが、複数の編集操作を行ってそのすべてのUndoableEditを個別にaddUndoすることもできます。
addUndoされた編集操作はデザイナーによってプロパティ変更とまとめた一単位のUndoとして扱われるのでUndo/Redo時には行った編集操作が全て巻き戻し(あるいは再生)されます。
プロパティ変更のイベントはUndo/Redo時にも発生するのでイベント内で編集操作を行ってaddUndoした場合は必ずPluginUtil#inUndoProcessをチェックしてUndo中はイベントをスキップさせなければなりません。
コンポーネントエディタはComponentEditorクラスのサブクラスとして作成します。
ComponentEditorは次のような処理が実装されています。
簡単に言えば「コンポーネント(マッパー関数)がダブルクリックされた時にどういう処理を行うか?」をプログラミングしたものがコンポーネントエディタです。
doActionメソッドはabstractメソッドとして定義されており、 プラグイン開発者がコンポーネントエディタの作成で実装しなければならないメソッドはこのメソッドのみです。
典型的なコンポーネントエディタの実装は以下のような処理の流れになります。
sampleフォルダのfcsample以下に標準のメールコンポーネントのプロパティをダイアログ上でまとめて 設定できるようにしたコンポーネントエディタのサンプルが入っています。
このサンプルはインスペクタの内容をダイアログに配置し直しただけなので、必ずしも必要ではありませんが 配置や説明を工夫するだけでもコンポーネントで設定すべき内容がわかりやすくなりますし、 コンポーネントエディタの作成に必要な技術要素がおおむね網羅されています。
このサンプルコードには以下の処理が含まれています。
意図的に平坦な実装にしてあるので、コードの解釈はそれほど難しくないですが以下に主な技術要素について説明を加えます。
コンポーネントエディタではコンポーネント(マッパー関数)自身の取得は、 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())); ...
CategoryPropertyは表形式で値を設定させるプロパティです。
CategoryPropertyでは列の情報はPropertyクラスで保持しており、各列の編集方法は定義ファイルでのProperty要素 定義によって決まります。
例えばMailコンポーネントの「追加するヘッダー」の定義は以下のようになっています。
<Category displayName="追加するヘッダー" key="Name" mapping="true" name="AdditionalHeaders" value="Default"> <Property choiceItem="Message-Id
Reply-To
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で作成可能でさえあれば作成して組み込むことが可能です。
作成したコンポーネントエディタを定義ファイルに組み込むにはListener要素を追加し、 class属性でクラス名を、menuItem属性で右クリックメニューに追加するキャプションを指定します。
<!-- メールアドレス形式のチェック --> <Listener class="com.infoteria.asteria.sample.plugin.MailEditor" menuItem="プロパティの編集"/>
このサンプルでは使用していませんが、setupメソッドをオーバーライドして
独自の定義項目を追加することも可能です。
(setupメソッドをオーバーライドする場合は先頭でsuper.setupを行ってください。)
汎用コネクションは任意の名前と値のセットをコネクション定義として保存しておける仕組みです。
これを利用して特殊な接続方法を使用するプロダクトと連携するようなコンポーネントを作成することができます。
(汎用コネクションについての説明はコンポーネント開発者ガイドにもあります。)
pluginCallはサーバー側で実装したコードをデザイナーから呼び出して何らかの情報を返すための仕組みです。
これらを組み合わせることで例えば特定のプロダクトからAPI経由で情報を取得し、デザイナーにその情報を反映
させるようなプラグインを作成することができます。
通常プロダクトのAPIの実行のためには専用のjarファイルが必要となるため、デザイナーから直接APIを実行するように
作成した場合jarファイルの配置等のインストール作業が煩雑になりますが、この方法であればプロダクトのjarファイル
はサーバー側だけに配置すれば良くなります。
サーバー側での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をラップしているだけですので作り方次第でどのような情報でもデザイナー側に返すことができます。
デザイナー側では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を定義することによって、
インスペクタ上でのドロップダウンリスト形式のプロパティとすることができます。
プラグインの作成ではすべての機能を自分で作成することもできますが、標準で提供されている部品を組み込むこともできます。
現在は以下の二つの部品のプラグインへの組み込み方法が公開されています。
いずれもRDB関連の補助UIクラスです。
これらのクラスはSQLToolというユーティリティクラスを介して使用することができます。
SQLBuilderはRDBGetコンポーネントで使用しているSELECT文を組み立てるためのUIです。
実際にはサーバー側のRDBGetコンポーネントが必要とするのはSQLのみなのでGUIでSELECT文を組み立てるという要件は必須ではありませんが、
これがあることによってユーザーはSQL文とそのフィールド定義を手動で行う煩わしさから解放されます。
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を使用するコンポーネントが必ずこのセットのプロパティを持たなければならないわけではありませんが、
ほとんどの場合このようなプロパティ構成となります。
sampleフォルダーにある「SQLBuilderSample」を参照してください。
処理の流れとしては
となります。
値の設定にはSQLToolのユーティリティメソッドが使用できます。
ほとんどの場合SQLBuilderを組み込むプラグインのコードはこのサンプルそのままになります。
SQLBuilderで生成されるSQL文には文字列中に「?param1?」「$param2$」のような独自のパラメーターが埋め込まれています。
このSQL文はUtilityクラスのparseSQLメソッドを通すことによってJavaのPreparedStatementで実行可能な形式に変換されます。
具体的には以下の変換がおこなわれます。
SQL文を取得した後はPreparedStatementなどのJDBCのAPIを用いて処理を行ってください。
テーブル選択ダイアログはRDBPutコンポーネントで使用しているテーブルとその列を選択するためのUIです。
選択した列情報をフィールド定義やCategoryPropertyに設定することでフィールド定義を簡略化することができます。
テーブル選択ダイアログを起動するためには「テーブル情報を取得するためのコネクション」の情報が必要です。
また通常は選択したテーブル名を設定するためのプロパティ、選択した列を設定するためのフィールド定義(またはCategoryProperty)も
必要になります。
これらのプロパティを定義した定義ファイルは以下のようになります。
<!-- RDBPutの定義ファイルより抜粋 --> <Property connection="RDBConnection" displayName="コネクション名" required="true" mapping="false" name="Connection" toolTip="Connection" type="connection"/> <Property displayName="テーブル名" 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の方ではフィールド名とデータ型は変更できず、「キーにする」列のみ変更可能なように定義されています。
列情報をどこに設定するかはコンポーネントの種類によって変わってきますがほとんどの場合テーブル選択ダイアログ を使用するコンポーネントではこのようなプロパティ構成となります。
sampleフォルダーにある「TableSelectSample」を参照してください。
処理の流れとしては
となります。
値の設定にはSQLToolのユーティリティメソッドが使用できます。
列情報をどこに設定するかはコンポーネントの種類によって変わってきますがほとんどの場合テーブル選択ダイアログ
を組み込むプラグインのコードはこのサンプルのようなコードになります。
外部アプリケーションを起動してその情報をデザイナーに取り込む場合は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で書き出したファイルが更新されていたらその内容をプロパティに書き戻す |
つまり上記定義の場合の実際の処理の流れは以下のようになります。
外部アプリケーションの起動中は以下のようなダイアログが表示され、デザイナーは外部アプリケーションが 終了するまでロックします。
サブクラスを作成する場合、preExecute/postExecuteをオーバーライドして処理を行います。
(例えばCategoryPropertyやFieldDefinitionを読み書きするなどの処理が行えます。)
プロパティ値の取得や設定の方法自体はコンポーネントエディタの場合と同じです。
便宜上ここでは説明項目を分けていますが、実際には外部アプリケーション起動はコンポーネントエディタの一種です。
つまりコンポーネントエディタのdoActionメソッドの実装が以下の処理になっているものが外部アプリケーション起動です。
外部アプリケーション起動のサンプルは現在ありませんが興味のある方は御相談ください。