本記事は,misima 旧字・旧仮名遣い表記変換サーバ(新字体・現代仮名遣い表記の入力文字列を旧字体・歴史的仮名遣い表記に変換する)と連携して RESTful Web Service を実現するメモである。
Web Service 概要
Web Service は Web HTTP 通信基盤を用いている限りにおいて,通信エンドのプラットフォーム・言語に依存しないサービスが実現できる。通常の Web アプリケーションは Web ブラウザが主なクライアントとなるわけだが,
Web Service はかつては SOAP (Simple Object Access Protocol) プロトコルで通信する形態が一般的だったが,最近では REST (Representational State Transfer) がメジャーである。
私はかつて misima SOAP Web Service を公開し,
きわめてシンプルな REST アプリケーション例である。既存の BSD Socket インタフェースのサーバと Web Service を接続する JAX-
Apache-Wink 1.4
Apache-
misima RESTful Web Service の設計
misima RESTful Web Service の基本設計は以下のとおりである。
- クライアント要求データを XML フォーマットで POST メソッドで受け取り,変換操作を加え,クライアントに返却する。
(misimaRESTful. java) - Wink REST Servlet に misimaRESTful クラスを登録するための Application 継承クラスを作成する。
(misimaRESTfulApplication. java) - クライアント要求データ: 変換対象テキスト,変換オプション,ユーザコード(限定公開ユーザを認証するための秘密コード)の XML データは以下のフォーマットとする。
<?xml version="1.0" encoding="UTF-8"?> <misimaRESTful> <misimaParam>misima変換オプション</misimaParam> <misimaTarget>変換対象テキスト</misimaTarget> <misimaUsercode>ユーザコード</misimaUsercode> </misimaRESTful>
これを JAXB アノテーション @XmlRootElement を介して Java Bean に取得する。(misimaXml. java) - 変換操作そのものは,既存の misimaserver 旧字・旧仮名遣い変換デーモンプログラムにソケット接続してこれを行う。
(misimaSocket. java) - misimaserver ホスト,ポート番号などの初期設定変数は外部プロパティ・ファイルを介して与える。
(misimaProperties. java) - ロギングは Log4j を使用する。(ここでは詳細割愛)
- Apache Tomcat 7.0 コンテナにデプロイ(配備)して運用する。
misimaRESTful Web Service 概念図
Eclipse 開発準備作業
Java EE 対応 Eclipse(Eclipse Java EE IDE for Web Developers - 現時点の最新バージョンは Kepler ケプラー)で本システムを開発することとした。Eclipse 画面にて,
図 1. Eclipse 新規プロジェクト作成
Apache-
- wink-server-1.4.jar (Apache Wink Server API)
- wink-common-1.4.jar (Apache Wink Common API)
- geronimo-jaxrs_1.1_spec-1.0.jar (JAX-RS 1.1 API)
- jaxb-api-2.2.jar (JAXB API)
- jaxb-impl-2.2.1.1.jar (JAXB Implementation)
- slf4j-api-1.6.1.jar (slf4j API)
- slf4j-simple-1.6.1.jar (slf4j API)
- log4j-1.2.17.jar (Log4j)
図 2. Eclipse lib Import
Java プログラムコード
src - (default package) に Java ソースを作成する。
misimaRESTful クラスは,misima RESTful Web Service の中核クラスとなる JAX-
@POST @Produces("text/plain; charset=UTF-8") public String misimaConvert(misimaXml ixml)
POST で選択されるメソッドであるということを @POST アノテーションで,返却されるデータ形式が text/
import javax.ws.rs.*; import javax.ws.rs.core.*; import javax.servlet.http.*; import org.apache.log4j.*; /** * misimaRESTful.java - misima RESTful Web Service サーバ * Web Service クライアントからの要求を受け取り,旧仮名・旧字変換支援 * misimaserver サーバにソケットで接続して,変換し,結果をクライアントに返信する。 * Copyright(c) 2014, isao yasuda, All Rights Reserved. */ @Path("/convert") @Consumes({"application/xml", "text/xml"}) public class misimaRESTful { /** misimaserver host */ private static String HOST = "localhost"; /** misimaserver port */ private static int PORT = 34000; /** MAX テキストサイズ */ private static int MAXSIZE = 8000; /** User code */ private static String USERCODE = "noxinsomniae201402"; /** プロパティファイル */ private static String prop = "misima.properties"; /** ログファイル */ private static Logger mslog = Logger.getLogger(misimaRESTful.class.getName()); /** リクエスト */ @Context private HttpServletRequest req; /** * 初期処理 * - プロパティ入力 * - 開始メッセージ */ static { try { misimaProperties mprop = new misimaProperties(prop); String hostp = mprop.getValue("hostname"); if (hostp != null) HOST = hostp; String portno = mprop.getValue("port"); if (portno != null) PORT = Integer.parseInt(portno); String maxsz = mprop.getValue("maxsize"); if (maxsz != null) MAXSIZE = Integer.parseInt(maxsz); String usercd = mprop.getValue("usercode"); if (usercd != null) USERCODE = usercd; } catch (Exception e) { mslog.error("Error occured in handling properties: " + e.getMessage()); } finally { mslog.info("misimaRESTful server initialization."); mslog.info("- misimaserver host: " + HOST); mslog.info("- misimaserver port: " + PORT); mslog.info("- max input size: " + MAXSIZE); mslog.info("- usercode: " + USERCODE); } } /** * misima 変換サービス * @param ixml 変換対象XML * @return ct 変換結果テキスト */ @POST @Produces("text/plain; charset=UTF-8") public String misimaConvert(misimaXml ixml) { /** 対象パラメータを取得 */ String misimaParam = ixml.getmisimaParam(); String misimaTarget = ixml.getmisimaTarget(); String misimaUsercode = ixml.getmisimaUsercode(); /** 変換結果 */ String ct = null; /** 1. リモートクライアント IP アドレス取得 */ mslog.info("* Client IP: " + req.getRemoteAddr()); /** 2. user code チェック */ if (misimaParam == null) { misimaParam = "-qkyit -s c"; } if (misimaTarget == null) { misimaTarget = ""; } if (misimaUsercode == null) { misimaUsercode = ""; } if (!misimaUsercode.equals(USERCODE)) { ct = "\nmisima ERROR: not authorized.\n" + misimaTarget; mslog.info("Usercode unmatched: " + misimaUsercode); return ct; } /** 3. 入力テキストサイズチェック */ int len = misimaTarget.length(); if (len > MAXSIZE) { ct = "\nmisima ERROR: text (" + len + ") has exceeded MAXSIZE (" + MAXSIZE + ").\n" + misimaTarget; mslog.info("Input Text too large: " + len + "."); return ct; } mslog.info("- Params: " + misimaParam + "; length: " + len); mslog.info("- Target: |" + misimaTarget + "|"); /** 4. misimaserver 送信メッセージ生成 */ StringBuffer mes = new StringBuffer(); mes.append("<misima_param>" + misimaParam + "</misima_param>" + misimaTarget); /** 5. misimaserver 接続ソケット生成 */ misimaSocket ms = new misimaSocket(HOST, PORT); try { ct = ms.convert(mes.toString()); } /** 6. 接続エラーの場合, ログにエラー出力 */ catch (Exception e) { ct = "\nmisima ERROR: " + e.getMessage() + "\n" + misimaTarget; mslog.error("Error occured in connection: " + e.getMessage()); } finally { ms = null; } /** 7. 結果を返して終了 */ mslog.info("- Result: |" + ct + "|"); return ct; } }
misimaRESTfulApplication クラスは,misimaRESTful リソースを Wink JAX-
import java.util.HashSet; import java.util.Set; import javax.ws.rs.core.Application; /** * misimaRESTfulApplication.java - REST Application * misimaRESTful クラスを REST リソースに登録する * Copyright(c) 2014, isao yasuda, All Rights Reserved. */ public class misimaRESTfulApplication extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> classes = new HashSet<Class<?>>(); classes.add(misimaRESTful.class); return classes; } }
misimaXml は,ユーザクライアントから送信された XML データを格納する Java Beans クラスである。
import javax.xml.bind.annotation.*; /** * misimaXml.java - XML 要求データ Java Beans * Web Service クライアントからの要求を格納する Bean * Copyright(c) 2014, isao yasuda, All Rights Reserved. */ @XmlRootElement(name="misimaRESTful") public class misimaXml { /** 変換オプション */ private String misimaParam; /** 変換対象テキスト */ private String misimaTarget; /** ユーザコード */ private String misimaUsercode; /** Constructors */ public misimaXml(String misimaParam, String misimaTarget, String misimaUsercode) { this.misimaParam = misimaParam; this.misimaTarget = misimaTarget; this.misimaUsercode = misimaUsercode; } public misimaXml() {} /** Getter */ public String getmisimaParam() { return misimaParam; } public String getmisimaTarget() { return misimaTarget; } public String getmisimaUsercode() { return misimaUsercode; } /** Setter */ public void setmisimaParam(String misimaParam) { this.misimaParam = misimaParam; } public void setmisimaTarget(String misimaTarget) { this.misimaTarget = misimaTarget; } public void setmisimaUsercode(String misimaUsercode) { this.misimaUsercode = misimaUsercode; } }
misimaSocket は,
import java.io.*; import java.util.regex.*; import java.net.*; import org.apache.log4j.*; /** * misimaSocket.java - misimasever ソケットクライアント * 受け取った文字列を,旧仮名・旧字変換支援 misimaserver にソケット * 接続して,変換し,結果をコール元に返却する。 * Copyright(c) 2014, isao yasuda, All Rights Reserved. */ public class misimaSocket { /** Server host */ private static String HOST; /** Server port */ private static int PORT; /** Socket */ private Socket sock; /** ロガー */ private static Logger mslog = Logger.getLogger(misimaSocket.class.getName()); /** Constructors */ misimaSocket(String host, int port) { HOST = host; PORT = port; sock = null; } misimaSocket() { HOST = "localhost"; PORT = 34000; sock = null; } /** * misimaserver 変換 * @param message misima 変換対象メッセージ * @return 変換結果テキスト */ public String convert(String message) throws IOException { /** \n を :#;~ に置換 - misimaserver 仕様による */ Pattern ptn = Pattern.compile("\n"); Matcher mtc = ptn.matcher(message); String sbuf = mtc.replaceAll(":#;~"); StringBuffer rbuf = new StringBuffer(); /** ソケット接続 */ try { sock = new Socket(HOST, PORT); /** データを送信する */ Writer send = new OutputStreamWriter( sock.getOutputStream(), "UTF-8"); send.write(sbuf + "\n"); send.flush(); /** データを受信する */ InputStreamReader recv = new InputStreamReader( new BufferedInputStream(sock.getInputStream()), "UTF-8"); int c; while ((c = recv.read()) != -1) { rbuf.append((char)c); } } catch (Exception e) { mslog.error(e.printStackTrace()); return "Error occured in misimaSocket: " + e.getMessage(); } finally { if (sock != null) sock.close(); sock = null; } /** 行末 \n を削除 */ ptn = Pattern.compile("\n"); mtc = ptn.matcher(rbuf.toString()); String ct = mtc.replaceAll(""); /** :#;~ を \n に復元 */ ptn = Pattern.compile(":#;~"); mtc = ptn.matcher(ct); return mtc.replaceAll("\n"); } }
misimaProperties は,外部ファイルに記述したプロパティを取得するクラスである。
import java.io.IOException; import java.util.Properties; /** * misimaProperties.java - プロパティを提供する * Copyright(c) 2014, isao yasuda, All Rights Reserved. */ public class misimaProperties { private Properties conf = null; /** Constructor 引数 properties file name */ public misimaProperties(String propfile) throws IOException { conf = new Properties(); conf.load(this.getClass(). getResourceAsStream(propfile)); } /** Key 値のプロパティを取得 */ public String getValue(String key) { return conf.getProperty(key); } }
プロパティ・ファイルの準備
プロパティ・ファイル misima.
# misimaRESTful properties hostname=localhost port=34000 maxsize=8000 usercode=noxinsomniae201402
web.xml 準備・デプロイ
Eclipse はコーディングしつつ,コンパイルエラーが確認できる。エラーが解消されたら,デプロイメント・ディスクリプタ web.
servlet-
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <display-name>misima RESTful Web Service</display-name> <description>misima Convert for RESTful Web Service</description> <servlet> <servlet-name>misimaRESTful</servlet-name> <servlet-class> org.apache.wink.server.internal.servlet.RestServlet </servlet-class> <init-param> <param-name>javax.ws.rs.Application</param-name> <param-value>misimaRESTfulApplication</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>misimaRESTful</servlet-name> <url-pattern>/misima/*</url-pattern> </servlet-mapping> </web-app>
これでデプロイ(配備)の準備ができた。
Tomcat 7.0 のマネージャ画面(http:
図 3. Tomcat 7 デプロイ・マネージャ画面
curl を用いた試験
配備が完了したら,端末(ターミナル)から curl コマンドで試験してみる。カレント・ディレクトリに test.
<?xml version="1.0" encoding="UTF-8"?> <misimaRESTful> <misimaParam>-q -kyit -s c</misimaParam> <misimaTarget>森鴎外はこう言い,内田百間を団扇であおいだ。 </misimaTarget> <misimaUsercode>noxinsomniae201402</misimaUsercode> </misimaRESTful>
さて,実行。
$ curl -X POST -H 'Content-Type: text/xml' \ --data-binary @test.xml \ http://yasuda.homeip.net:8080/misimaRest/misima/convert 森鷗外はかう言ひ,內田百閒を團扇であふいだ。 $
旧字・旧仮名遣い変換されたテキストが表示され,成功である。
シェルスクリプト
curl コマンドを使えば,以下のようなシェルスクリプトを作成しておくと,変換対象テキストを標準入力から食わせて変換できる。
#!/bin/sh # misimaRESTful curl script URL=http://yasuda.homeip.net:8080/misimaRest/misima/convert IT=`awk '{ print $0 }' < /dev/stdin` XML="<?xml version=\"1.0\" encoding=\"UTF-8\"?>\ <misimaRESTful>\ <misimaParam>-q $*</misimaParam>\ <misimaTarget>${IT}</misimaTarget>\ <misimaUsercode>noxinsomniae201402</misimaUsercode>\ </misimaRESTful>" curl -X POST -H 'Content-Type: text/xml' --data-binary "${XML}" ${URL} echo ""
実行は以下のとおり。
$ cat test.txt 森鴎外はこう言い, 内田百間を団扇であおいだ。 $ misimaRESTful.sh -kyitn -s c < test.txt 森鷗外ハカウ言ヒ, 內田百閒ヲ團扇デアフイダ。 $
Web ブラウザからの利用
ブラウザから変換できるページ を用意したので,試していただきたい。変換の様子のスクリーンショットを以下に示す。ただし,misimaRESTful Web Service は,しばらく公開したのち,削除ないし,ユーザコードを変更して限定公開とするので悪しからず。
misima RESTful Web Service ブラウザ版
misima RESTful Web Service へのアクセスは jQuery ajax 関数を用いた非同期 POST による。フォームの入力から XML を組立てている。HTML 及び JavaScript を以下に掲載しておく(一部省略しているところがある)。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <link rel="stylesheet" type="text/css" href="./css/misima-style.css" /> <title>misima RESTful Web Service</title> </head> <body> <div style="width: 470px; text-align: center;"> <h2>misima 旧字・旧仮名遣い変換 RESTful Web Service</h2> </div> <div class="option"> <select id="bopt"> <option value="1" selected>旧字/旧仮名/用語/繰返</option> <option value="2">旧字/旧仮名/用語</option> <option value="3">旧字/旧仮名</option> <option value="4">旧字</option> </select> <input type="checkbox" id="topt" />LaTeX <input type="checkbox" id="nopt" />仮名反転 <input type="checkbox" id="lopt" />簡体字 <input type="checkbox" id="dopt" />形態素解析出力<br /> <textarea id="obj" cols="40" rows="20" style="width: 470px; height: 84px; overflow: auto;"></textarea> </div> <div style="width: 470px;"> <div style="float: left; width: 70%"> Usercode: <input type="text" id="ucode" size="30" value="noxinsomniae201402" /> </div> <div style="float: left; text-align: right; width: 30%"> <input type="button" value="変 換" onclick="misimaConvert();" /> <input type="button" value="クリア" onclick="clearText();" /> </div> </div> <div id="status"></div> <div id="out"></div><!-- ここに変換結果を挿入 --> <!-- JavaScript Library --> <script type="text/javascript" src="./js/jquery-1.10.2.min.js"></script> <script type="text/javascript" src="./js/misima-rest.js"></script> </body> </html>
/* misima RESTful Web Service convert 2014(c) isao yasuda, All Rights Reserved. */ // Global Valuables var postUrl = "/misimaRest/misima/convert"; // Server path var maxSize = 8000; // Max text size var xmlQuery; // XML query to be posted // サーバに検索リクエストを非同期 POST する function invokeServer() { // processing $('#status').html("misima 処理中..."); // Ajax Communication to POST XML data $.ajax({ url: postUrl, // misima RESTful Web Service URL type: "POST", contentType: "application/xml; charset=UTF-8", dataType: "text", cache: false, data: xmlQuery }).done(function(result, textStatus, jqXHR) { // 通信成功 result = result.replace(/\r?\n/g, "<br />"); $('#out').html(result); // 変換結果挿入 $('#status').html("Normally ended."); }).error(function(jqXHR, textStatus, errorThrown) { // 通信失敗 $('#status').html("Error occured. status: " + textStatus + " thrown: " + errorThrown); }).complete(function(jqXHR, textStatus) { // 通信完了 window.status = "misimaConvert completed, textStatus: " + textStatus; }); } // misima 変換処理を実行する function misimaConvert() { // 変換対象テキストを取り出し,テキスト長をチェック var it = $('#obj').val(); if (it.length == 0) { alert("対象テキストを指定してください"); return; } if (it.length > maxSize) { alert("入力テキストが最大値" + maxSize + "を越えています"); return; } // Usercodeを取り出す var uc = $('#ucode').val(); if (uc.length == 0) { alert("Usercode を指定してください"); return; } // misima オプション + テキスト(改行文字変換) で電文を作成する var ps = makeParams(); var pt = /-x /; var le = ""; if (pt.test(ps)) { le = it.replace(/\r?\n/g, " :#;~" ); } else { le = it.replace(/\r?\n/g, ":#;~"); } // XML を組み立てる xmlQuery = '<?xml version="1.0" encoding="UTF-8"?>' + '<misimaRESTful>' + '<misimaParam>' + ps + '</misimaParam>' + '<misimaTarget>' + le + '</misimaTarget>' + '<misimaUsercode>' + uc + '</misimaUsercode>' + '</misimaRESTful>'; // サーバに要求を出す invokeServer(); } // misima オプションを組み立てる function makeParams() { var params = "-q"; // 初期値: quiet mode // -s:旧字/-k:旧仮名/-y:用字/-t:単純補正/-i:繰返符号変換 var bopt = $('#bopt').children(':selected').val(); switch (bopt) { case "1": params += " -kyit -s c"; break; case "2": params += " -kyt -s c"; break; case "3": params += " -kt -s c"; break; case "4": params += " -t -s c"; break; default: break; } // LaTeX 変換: -x 漢文・多国語サポート, ドイツ語モード params += ($('#topt').prop('checked')) ? " -x kuit -g" : ""; // 仮名反転: -n params += ($('#nopt').prop('checked')) ? " -n" : ""; // 簡体字-繁体字変換: -l params += ($('#lopt').prop('checked')) ? " -l" : ""; // 形態素解析出力: -d params += ($('#dopt').prop('checked')) ? " -d" : ""; return params; } // 入力テキストをクリアする function clearText() { $('#obj').val(''); } // 初期状態のフォーカス $(function() { $('#obj').focus(); });
参考文献
JAX-RS RESTful Web Service について詳しく解説している参考図書のなかで,何をおいてもまずは,オライリー,
misima RESTful Web Service クライアントの実装例 [ 付記 ]
上記に,Web ブラウザから利用するための JavaScript 及び,curl コマンドを用いたコマンドライン型クライアントを示した。本記事を書いたのち,いくつかのアプリケーションから misima RESTful Web Service を利用するためのクライアントの実装例を試作した。以下にそれら記事へのリンクを設置しておく。
- GNU Emacs クライアント (Emacs Lisp url)
- Microsoft Word クライアント (VBA XMLHTTP)
- Microsoft Office 2007 Excel / Word クライアント (VBA XMLHTTP)
- Windows 秀丸エディタクライアント (秀丸マクロ XMLHTTP)
- Java クライアント (Apache Wink)
- Perl クライアント (REST::Client)
[ 2015/02/02 付記 ]
misima RESTful Web Service は 2014/12 より友人のためだけの限定公開といたしました。上記ユーザ認証コードでは動作しません。