No.14 RESTでCorticonにリクエストをしてみよう ~ディシジョン・サービスを呼び出すJavaによるRESTクライアントの説明~
2017.04.12 Progress Corticon
本エントリーは株式会社アシスト様が寄稿したエントリー(https://www.ashisuto.co.jp/product/category/brms/progress_corticon/column/detail/brmstech14.html)を転載したものとなります。
|
Corticon がRESTのJSON送受信にも対応しました。REST通信は、Corticon V5.5以降で利用できます。もちろんこれまで通りSOAPのXML送受信も利用できるのでアプリケーション連携の幅が広がりました。そこで、今回は、RESTによるディシジョン・サービスの呼び出し方を説明します。
CorticonをRESTで呼び出すと
・SOAPよりも少し処理速度が速い
・SOAPでは送受信することができなかったUnicode拡張領域の文字も送受信できる
といったメリットがあります。(*1)
しかし、RESTの場合はCorticonにRESTのサービス仕様を作成する機能がないため、JSONとクライアント内オブジェクトをマッピングするのにSOAPよりも少し手間がかかります。(*2)
今回の記事では、JSONとクライアント内オブジェクトをマッピングする方法の一例として、Jacksonライブラリを利用したJavaのRESTクライアントのサンプルを紹介します。
(*1) Corticon.5.5.2ではUnicode拡張領域の文字に対して一部のルール演算子を使用すると正しい結果を返しません。
例: ルール演算子「size」を使用した場合、対象文字列にサロゲートペアが入っていると1文字を2文字としてカウントします。
サロゲートペアを使いたい場合は、サロゲートペアに対応した拡張演算子を作成することで対応できます。
(*2) CorticonにはSOAPのサービス仕様であるWSDLを作成する機能があるため、各種ライブラリ/フレームワーク/アプリケーションのWSDLを読み込んで通信を簡略化する機能を使用することができます。対して、CorticonにはRESTやJSONのサービス仕様(例えば「WADL」や「JSON Schema」)を作成する機能はないので、各種ライブラリ/フレームワーク/アプリケーションのそれらを取り込んで通信を簡略化する機能を使用することができません。このあたりの設定方法も説明します。
ルールの実装
[記事執筆環境]
Corticon 5.5.2.7
Oracle JDK 1.7
Jackson 1.9
Apache HttpClient 4.5
最初に、今回の記事で使用するサンプルルールをCorticon Studioで作成します。
■ 語彙(ecore)
今回の記事で使用する語彙(ecore)は以下です。
エンティティ名 | XML要素名 | |
親 | Parent | |
属性名 | XML要素名 | 型 |
文字列 | Svalue | 文字列 |
整数 | Ivalue | 整数 |
日付 | Dvalue | Date |
関連性名 | XML要素名 | カーディナリティ, 関連方向性 |
子 | Children | 1->* , 親->子 |
エンティティ名 | XML要素名 | |
子 | Children | |
属性名 | XML要素名 | 型 |
文字列 | Svalue | 文字列 |
整数 | Ivalue | 整数 |
日付 | Dvalue | Date |
<Corticon Studioでの表示>
|
この語彙(ecore)は、RESTの動作確認用の抽象的なもので、何か意味があるものではありません。
注目点としては、全項目のXML要素名にアルファベットを設定しています。
この設定で定義したXML要素名は、SOAPのXML送受信時にはXML内の要素名として適用されますが、RESTのJSON送受信にはJSON内の要素名としても適用されます。
CorticonはJSON要素名を日本語で定義してもかまいませんが、一般的なRESTサービスやRESTクライアントでは英数字のみの場合が多いため、ここでも全てアルファベットで設定しています。
■ルールシート(ers)とルールフロー(erf)
ルールシートは以下の3つを作成しました。
|
|
|
この3つのルールは、RESTの動作確認用の抽象的なもので、何か意味があるものではありません。
この3つのルールシート(ers)を配置したルールフロー(erf)を作成し、Corticon Serverにディシジョン・サービスとしてデプロイします。
ディシジョン・サービス名は「JsonTest」にします。
ディシジョン・サービス名を日本語にするとRESTでは利用できないので注意してください。
■ルールテスト(ert)
Corticon Studioのテスト機能で、RESTのJSONペイロードの構造を確認することができます。
例えば以下のようなルールテスト(ert)を作成します。
|
Corticon Studioメニュー「ルールテスト」-「テストシート」-「データ」の「入力」-「要求をJSONでエクスポート」を実行すると以下のようなJSONが得られます。
Corticon Serverへ送信するリクエストJSONも同じ構造にする必要があるため、RESTクライアントのプログラムを実装する際の参考になります。
{"Objects ": [{ "Svalue": null, "Ivalue": null, "children": [ { "Svalue": "テスト", "Ivalue": "1", "__metadata": { "#id": "子_id_1", "#type": "child" }, "Dvalue": "2016-11-11" }, { "Svalue": "テスト2", "Ivalue": "2", "__metadata": { "#id": "子_id_2", "#type": "child" }, "Dvalue": "2016-12-12" } ], "__metadata": { "#id": "親_id_1", "#type": "parent" }, "Dvalue": null }]}
Corticon Studioメニュー「ルールテスト」-「テストシート」-「データ」の「出力」-「応答をJSONでエクスポート」を実行すると以下のようなJSONが得られます。
入力のJSONと基本的には同じ構造ですが、ルールメッセージで出力される項目が追加されています。
Corticon Serverから返ってくるレスポンスJSONも同じ構造になるため、RESTクライアントのプログラムを実装する際の参考になります。
{ "Messages ": { "Message": [ { "entityReference": "親_id_1", "text": "親の整数を初期化します。", "severity": "Info", "__metadata": {"#type": "#RuleMessage"} }, { "entityReference": "親_id_1", "text": "子の整数:1を親の整数に足します。", "severity": "Info", "__metadata": {"#type": "#RuleMessage"} }, { "entityReference": "親_id_1", "text": "子の整数:2を親の整数に足します。", "severity": "Info", "__metadata": {"#type": "#RuleMessage"} }, { "entityReference": "親_id_1", "text": "親の文字列を初期化します。", "severity": "Info", "__metadata": {"#type": "#RuleMessage"} }, { "entityReference": "親_id_1", "text": "子の文字列:テストを親の文字列に足します。", "severity": "Info", "__metadata": {"#type": "#RuleMessage"} }, { "entityReference": "親_id_1", "text": "子の文字列:テスト2を親の文字列に足します。", "severity": "Info", "__metadata": {"#type": "#RuleMessage"} }, { "entityReference": "親_id_1", "text": "親の日付を初期化します。", "severity": "Info", "__metadata": {"#type": "#RuleMessage"} }, { "entityReference": "親_id_1", "text": "子の日付:2016-11-11を親の日付にします。", "severity": "Info", "__metadata": {"#type": "#RuleMessage"} }, { "entityReference": "親_id_1", "text": "子の日付:2016-12-12を親の日付にします。", "severity": "Info", "__metadata": {"#type": "#RuleMessage"} } ], "__metadata": {"#type": "#RuleMessages"}, "version": 0 }, "Objects": [{ "Svalue": "テストテスト2", "Ivalue": "3", "children": [ { "Svalue": "テスト", "Ivalue": "1", "__metadata": { "#id": "子_id_1", "#type": "child" }, "Dvalue": "2016-11-11" }, { "Svalue": "テスト2", "Ivalue": "2", "__metadata": { "#id": "子_id_2", "#type": "child" }, "Dvalue": "2016-12-12" } ], "__metadata": { "#id": "親_id_1", "#type": "parent" }, "Dvalue": "2016-12-12" }] }
RESTクライアントの作成 (Java, Jackson)
本記事では、JSONとJavaオブジェクトをマッピングして相互変換するためのライブラリとしてJackson 1.9を使用します。
またHTTP通信を行うためのライブラリとしてApache HttpClient 4.5を使用します。
https://wiki.fasterxml.com/JacksonHome
https://hc.apache.org/index.html
もし他の言語や他のフレームワーク/ライブラリを使用する場合は、それぞれのフレームワーク/ライブラリの方法にしたがってください。
基本的には同様の考え方で実装可能です。
■JSONマッピング用クラス
前述のCorticon Studioのテストシートで作成した入出力JSONデータを参考に、JSONデータとマッピングするために必要なPOJOクラスを作成していきます。
まずは、汎用的で基本的なクラスとして、以下のPayload.java, Messages.java, Message.java, __metadata.javaを作成します。
<Payload.java>
package SampleVocabulary ; import java.util.List; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.map.annotate.JsonSerialize; @JsonIgnoreProperties(ignoreUnknown=true) @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class Payload { @JsonProperty("Objects") private Listparents; @JsonProperty("Messages") private Messages messages; public List getParents() { return parents; } public void setParents(List _parents) { this.parents = _parents; } public Messages getMessages() { return messages; } public void setMessages(Messages _messages) { this.messages = _messages; } }
<Messages.java>
package SampleVocabulary ; import java.util.List; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.map.annotate.JsonSerialize; @JsonIgnoreProperties(ignoreUnknown=true) @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class Messages { @JsonProperty("Message") private Listmessage; @JsonProperty("__metadata") private __metadata metadata; private String version; public List getMessage() { return message; } public void setMessage(List _message) { this.message = _message; } public __metadata getMetadata() { return metadata; } public void setMetadata(__metadata _metadata) { this.metadata = _metadata; } public String getVersion() { return version; } public void setVersion(String _version) { this.version = _version; } }
<Message.java>
package SampleVocabulary ; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.map.annotate.JsonSerialize; @JsonIgnoreProperties(ignoreUnknown=true) @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class Message { private String entityReference; private String text; private String severity; @JsonProperty("__metadata") private __metadata metadata; public String getEntityReference() { return entityReference; } public void setEntityReference(String _entityReference) { this.entityReference = _entityReference; } public String getText() { return text; } public void setText(String _text) { this.text = _text; } public String getSeverity() { return severity; } public void setSeverity(String _severity) { this.severity = _severity; } public __metadata getMetadata() { return metadata; } public void setMetadata(__metadata _metadata) { this.metadata = _metadata; } }
<__metadata.java>
package SampleVocabulary ; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.map.annotate.JsonSerialize; @JsonIgnoreProperties(ignoreUnknown=true) @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class __metadata { @JsonProperty("#id") private String id; @JsonProperty("#type") private String type; public String getId() { return id; } public void setId(String _id) { this.id = _id; } public String getType() { return type; } public void setType(String _type) { this.type = _type; } }
Corticon Studioのルールテストで作成したJSONと比較すると、それぞれのクラスとメソッドがJSONのどの項目と対応しているか確認することができます。
また、それぞれのクラスでJackson特有のアノテーションによって必要な設定を行っています。特に、JSONデータ名とメンバ変数名が異なる場合には、「@JsonProperty」の設定でマッピングを行う必要があります。
なお、これらのクラスは、Corticon ServerのJSONペイロードの汎用的で基本的な部分になりますので、他のディシジョン・サービスを利用する場合でも、同じクラスを再利用することができます。また、語彙(ecore)やルールに変更があった場合でも、これらのクラスは特に修正する必要はありません。
次に、語彙クラスを作成します。JSON内の語彙(ecore)の構造に依存する箇所とマッピングするためのクラスです。
以下のparent.java, child.javaになります。
<parent.java>
package SampleVocabulary ; import java.util.Date; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.map.annotate.JsonSerialize; @JsonIgnoreProperties(ignoreUnknown=true) @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class parent { @JsonProperty("Ivalue") private Integer ivalue; @JsonProperty("Svalue") private String svalue; @JsonProperty("Dvalue") private Date dvalue; @JsonProperty("children") private child[] children; @JsonProperty("__metadata") private __metadata metadata; public Integer getIvalue() { return ivalue; } public void setIvalue(Integer _ivalue) { this.ivalue = _ivalue; } public String getSvalue() { return svalue; } public void setSvalue(String _svalue) { this.svalue = _svalue; } public Date getDvalue() { return dvalue; } public void setDvalue(Date _dvalue) { this.dvalue = _dvalue; } public child[] getChildren() { return children; } public void setChildren(child[] _children) { this.children = _children; } public __metadata getMetadata() { return metadata; } public void setMetadata(__metadata _metadata) { this.metadata = _metadata; } }
<child.java>
package SampleVocabulary ; import java.util.Date; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.map.annotate.JsonSerialize; @JsonIgnoreProperties(ignoreUnknown=true) @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class child { @JsonProperty("Ivalue") private Integer ivalue; @JsonProperty("Svalue") private String svalue; @JsonProperty("Dvalue") private Date dvalue; @JsonProperty("__metadata") private __metadata metadata; public Integer getIvalue() { return ivalue; } public void setIvalue(Integer _ivalue) { this.ivalue = _ivalue; } public String getSvalue() { return svalue; } public void setSvalue(String _svalue) { this.svalue = _svalue; } public Date getDvalue() { return dvalue; } public void setDvalue(Date _dvalue) { this.dvalue = _dvalue; } public __metadata getMetadata() { return metadata; } public void setMetadata(__metadata _metadata) { this.metadata = _metadata; } }
これらの語彙クラスは、Corticon Studioで作成した語彙(ecore)と同じ構造を持つように作成しています。語彙(ecore)の表と見比べて確認してください。
この語彙クラスの構造は、Axis2などを利用してWSDLから自動生成したSOAP用Stub内の同名クラスとほぼ同じです。
SOAP用Stubと異なる点は、Jackson特有のアノテーションを追加している箇所や、__metadata用のメンバをクラスに追加している箇所です。
対象ディシジョン・サービスを更新する場合、ルールシート(ers, erf)の内容の変更だけであれば、これらの語彙クラスを修正する必要はありません。
もし対象ディシジョン・サービスの語彙(ecore)に対して変更があった場合は、これらの語彙クラスも修正を行う必要があります。
■RESTクライアントのメインプログラム
最後にRESTクライアントのメインプログラムexecuteRestMain.javaを作成します。手続きとしては以下です。
1.リクエストObjectを作成する。
2.リクエストObjectをリクエストJSONに変換する。
3.適切なHTTPの設定を行う。
4.Corticon ServerにリクエストJSONをPOSTする。
5.Corticon ServerからレスポンスJSONを受け取る。
6.レスポンスJSONをレスポンスObjectに変換する。
リクエストObject, レスポンスObjectを生成しJSONと相互変換するために、前述のJSONマッピング用クラスとJacksonマッパーを使用しています。
<executeRestMain.java>
package executeRest ; import java.io.*; import java.text.*; import java.util.*; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.apache.http.impl.client.HttpClientBuilder; import org.codehaus.jackson.map.ObjectMapper; import SampleVocabulary.*; public class executeRestMain { private static final String CCSERVER_URL = "https://localhost:8080/axis/corticon/execute"; private static final String DECISIONSERVICE_NAME = "JsonTest"; private static final DateFormat SDF = new SimpleDateFormat("yyyy-MM-dd"); private static final String OUTPUT_PATH = "c:/temp/executeRest.log"; public static void main(String[] args) { try { // リクエスト用オブジェクトの作成 Payload requestObject = createSamplePayload(); outputPayload(requestObject); // オブジェクトの内容を出力 // リクエストオブジェクトをJSONに変換 ObjectMapper mapper = new ObjectMapper(); mapper.setDateFormat(SDF); // 送受信する日付書式はyyyy-MM-dd String requestJson = mapper.writeValueAsString(requestObject); // HTTPリクエスト HttpPost post = new HttpPost(CCSERVER_URL); // CcServerのREST用URL post.setHeader("dsName", DECISIONSERVICE_NAME); // ディシジョン・サービス名(英数字のみ) StringEntity content = new StringEntity(requestJson, "UTF-8"); // JSONの文字コードはUTF-8 content.setContentType("application/json; charset=UTF-8"); post.setEntity(content); HttpClient client = HttpClientBuilder.create().build(); HttpResponse response = client.execute(post); if (response.getStatusLine().getStatusCode() == 200) { // レスポンスJSONをオブジェクトに変換 String responseJson = EntityUtils.toString(response.getEntity(), "UTF-8"); Payload responseObject = mapper.readValue(responseJson, Payload.class); outputPayload(responseObject); // オブジェクトの内容を出力 } } catch (Exception e) { e.printStackTrace(); } System.exit(0); } private static void outputPayload(Payload oPayload) { try { FileOutputStream fos = new FileOutputStream(OUTPUT_PATH, true); PrintStream ps = new PrintStream(fos, true, "UTF-8"); System.setOut(ps); } catch (Exception e) { e.printStackTrace(); } String s = ""; // 親データ, 子データを出力 for (parent p: oPayload.getParents()) { s += "親ID: " + p.getMetadata().getId(); s += ", 整数: " + p.getIvalue(); s += ", 文字列: " + p.getSvalue(); if (p.getDvalue() != null) { s += ", 日付: " + SDF.format(p.getDvalue()); } s += "\n"; if (p.getChildren() == null) { continue; } for (child c: p.getChildren()) { s += " 子ID: " + c.getMetadata().getId(); s += ", 整数: " + c.getIvalue(); s += ", 文字列: " + c.getSvalue(); if (c.getDvalue() != null) { s += ", 日付: " + SDF.format(c.getDvalue()); } s += "\n"; } } System.out.println(s); // ルールメッセージを出力 if (oPayload.getMessages() == null || oPayload.getMessages().getMessage() == null) { return; } s = ""; for (Message m: oPayload.getMessages().getMessage()) { s += m.getSeverity(); s += ", " + m.getEntityReference(); s += ", " + m.getText(); s += "\n"; } System.out.println(s); } private static Payload createSamplePayload() throws ParseException { child[] cs = new child[2]; cs[0] = createChild(10, "[絵文字: ????\uD83D\uDE90]", SDF.parse("2011-01-02"), "c1"); cs[1] = createChild(20, "[難漢字: ????\uD867\uDE3D]", SDF.parse("2012-01-02"), "c2"); child[] cs2 = new child[2]; cs2[0] = createChild(30, "aaaaaaaaaa", SDF.parse("2013-10-11"),"c3"); cs2[1] = createChild(40, "bbbbbbbbbbb", SDF.parse("2014-11-12"), "c4"); ListlistParent = new ArrayList (); listParent.add(createParent(null, null, null, "p1", cs)); listParent.add(createParent(null, null, null, "p2", cs2)); Payload returnObject = new Payload(); returnObject.setParents(listParent); return returnObject; } private static parent createParent(Integer i, String s, Date d, String _id, child[] cs) { if (_id == null) { return null; } parent p = new parent(); if (i != null) p.setIvalue(i); if (s != null) p.setSvalue(s); if (d != null) p.setDvalue(d); if (cs != null) p.setChildren(cs); p.setMetadata(createMetadata(_id, "parent")); return p; } private static child createChild(Integer i, String s, Date d, String _id) { if (_id == null) { return null; } child c = new child(); if (i != null) c.setIvalue(i); if (s != null) c.setSvalue(s); if (d != null) c.setDvalue(d); c.setMetadata(createMetadata(_id, "child")); return c; } private static __metadata createMetadata(String _id, String _type) { __metadata m = new __metadata(); m.setId(_id); m.setType(_type); return m; } }
ポイントはプログラム内にもコメントとしても記述していますが、以下になります。
●プログラム自体の文字コードはUTF-8にしてください 。
●HTTPリクエストの設定に関して
►Corticon ServerのURLは適宜書き換えてください。
►Headerで指定するディシジョン・サービス名は英数字にしてください。
►Corticon Serverへ送受信するJSONの文字コードはUTF-8にしてください。
►HttpClientの詳細なログを出力するとデバッグの際に有効です。
►送受信したJSON文字列を出力するのもデバッグの際に有効です。
●ディシジョン・サービスへの入力値
►ここでは「createSamplePayload()」でハードコードしています。
►文字列の箇所はUnicode拡張領域の例として、難漢字と絵文字を入れています。
►絵文字の一個目は笑顔の絵文字を直接入力し、二個目はサロゲートペアで車の絵文字を指定しています。
►難漢字の一個目は「しかる」(口へんに七)を直接入力し、二個目はサロゲートペアで「ホッケ」(魚へんに花)を指定
しています。
●日付の書式に関して
►このプログラムではSOAPやRESTで最も一般的に使われる「yyyy-MM-dd」形式に全て統一しています。
►Corticon Server自体は他の日付書式でも受け付けます。設定で変更することも可能です。
●ディシジョン・サービスの実行結果
►ここでは「outputPayload()」でUTF-8テキストファイルに出力しています。
このプログラムのmainを実行し、出力テキスト(ここでは「C:/temp/executeRest.log」)をWebブラウザなどで表示させると、以下のようになります。
上6行がリクエストObjectの中身、その下がレスポンスObjectの中身です。入力値と出力値、ルールメッセージが期待通りに出ていることが確認できます。
<mainの実行結果 >
|