Liquidフレームワークを使ったアプリケーションの作成

ここではLiquidフレームワークを使ったサンプルアプリケーションの作り方を説明します。
今回作成するのはAjax+Cometを使ったリアルタイムチャットサービスです。

Actionクラス、テンプレートの作成

まず初めにチャットページを表示するためのActionクラス、テンプレートを作成します。

Actionクラス

LiquidではActionクラスをPOJOで作成するため、単純なActionであれば特定のクラスの継承、インターフェースの実装は必要とされません。

 package chat.action;
 
 public class ChatAction
 {
    public String index()
    {
       return "index.ftl";
    }
 }

メソッドが1つ作成されていますが、これについては後述します。その前にActionクラスとメソッドの関係を理解しましょう。
LiquidデフォルトのWebコンテナではURLの解決が次のように行われます。

 http://xxx.xxx/(プラグインバインド名)/(Actionクラス名)/(メソッド名)
このようにアクセスされたURL内のパスからプラグインバインド名(※plugin.xmlの項で説明します)、Actionクラス名、メソッド名を抽出し、目的のActionクラスが呼び出される仕組みになっています。
ですので今回作成したChatActionクラスであれば/ChatAction/indexというパスでアクセスする事により、ChatActionクラス内のindex()が呼び出されます。
Actionクラスには自由メソッドを実装して問題ありませんが、URLによりアクセスされるメソッドは戻り値にテンプレート名を指定する必要があります。
ここで指定されたテンプレートがメソッドの呼び出し後に表示されるので、次はこのテンプレートindex.ftlを作成しましょう。

テンプレート

 <html>
 <body>
 <p>Liquid</p>
 </body>
 </html>

LiquidではデフォルトでFreeMarkerテンプレートエンジンが有効になっていますので、FreeMarkerの記法を利用する事が可能です。
とりあえずこの状態で正常に表示が出来るか、Liquidを起動させて確認しましょう。次項ではLiquidの起動方法を説明します。

Liquidの起動に必要なもの

Liquidを起動するには、プラグインを処理するモジュールを定義したmodule.xmlとプラグイン本体を定義したplugin.xmlが必要です。

module.xmlの作成

まずはmodule.xmlを作りましょう。

 <?xml version="1.0" encoding="euc-jp"?>
 <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
 <beans>
 
     <bean id="moduleManager" class="jp.maru.liquid.kernel.ModuleManager">
         <property name="modules" >
             <list>
                 <bean id="httpTemplateServerModule"
                 class="jp.maru.liquid.kernel.modules.HttpServerModule">
                     <property name="connector" ref="defaultConnector" />
                     <property name="strategy">
                         <bean class="jp.maru.liquid.kernel.modules.strategy.HttpTemplateStrategy" />
                     </property>
                 </bean>
 
                 <bean id="asyncHttpTemplateServerModule"
                 class="jp.maru.liquid.kernel.modules.AsyncHttpServerModule">
                     <property name="connector" ref="defaultConnector" />
                     <property name="strategy">
                         <bean class="jp.maru.liquid.kernel.modules.strategy.HttpBasicStrategy" />
                     </property>
                 </bean>
             </list>
         </property>
     </bean>

     <bean id="defaultConnector"
      class="jp.maru.liquid.kernel.modules.http.HttpConnector">
         <property name="port" value="80" />
     </bean>
 </beans>

今回のChatアプリケーションではチャットページの表示処理と非同期の発言/リロード処理が発生するので、それぞれHttpServerModuleAsyncHttpServerModuleをロードします。チャットページではテンプレート処理が発生するのでストラテジとしてHttpTemplateStrategyを、非同期処理ではテンプレート処理は必要ないのでHttpBasicStrategyを選択しています。

plugin.xmlの作成

次にplugin.xmlを作成します。

 <?xml version="1.0" encoding="euc-jp"?>
 <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
 <beans>
 
     <bean id="pluginManager" class="jp.maru.liquid.kernel.PluginManager">
         <property name="plugins">
             <list>
                 <ref local="chatActionPlugin" />
             </list>
         </property>
     </bean>
 
     <bean id="chatActionPlugin"
      class="jp.maru.liquid.core.plugins.HttpActionPlugin">
         <property name="bindURI" value="/chat/*" />
         <property name="encoding" value="utf-8" />
         <property name="templatePath" value="/chat/www/" />
         <property name="actionClass" value="chat.action.*Action" />
     </bean>
 
 </beans>

独自のプラグインを作成せずともLiquidに組み込まれているHttpActionPluginを使えばActionクラスとテンプレートのみでWebアプリケーションを作成する事も可能です。
HttpActionPluginに以下のプロパティを設定します。
-bindURI - URLにバインドさせる名前。前述のプラグインバインド名。Actionを実行するには「/xxx/*」と最後をワイルドカードにする必要があります。 -encoding - HTTPヘッダのcharset、パラメータのエンコードに使用される文字コード。 -templatePath - テンプレートが置かれているベースのディレクトリパス。 -actionClass - URLへのアクセス時に呼び出されるActionクラス。アクション名をワイルドカードにより解決可能。

ファイルの配置

現在までに作成したファイルを配置します。今回は/chatというディレクトリを使います。
まずは必要となるサブディレクトリを以下のように作成しましょう。

 /chat
   /lib
   /src  
   /www 
   /classes
次にLiquidのアーカイブを展開し、liquid.jar及びlib以下のjarファイルを/chat/libの中に入れ、run.shを/chatへ移動させます。
そして先ほど作成したChatAction.javaを/chat/srcに、index.ftlを/chat/wwwに、module.xml、plugin.xmlを/chatへ配置します。

コンパイル

配置の終わったChatAction.javaをコンパイルし、classファイルを/chat/classesへ移動させます。
今回のActionクラスでは依存するパッケージが無いため、特にクラスパスの指定無しでコンパイルが行えますが、Liquid他のパッケージを参照する場合は適宜クラスパスを指定してコンパイルしましょう。

起動

Liquidの起動はrun.shを使います。単純にrun.shを実行するだけで起動するはずです。
起動が確認出来たらhttp://xxx.xxx/chat/Chat/indexにアクセスしてみて下さい。ちゃんとページが表示されましたか?

プラグインの作成

チャットページの作成ではLiquid組込みのプラグインを使ったので、新たにプラグインを作る必要は無かったですが、非同期にチャットメッセージを読み込むにはそれ専用の処理が必要になるのでプラグインを作成します。
Cometではノンブロッキング通信を行う必要があるため、今回作成するプラグインではノンブロッキング通信に対応したモジュールAsyncHttpServerModuleを利用します。AsyncHttpServerModuleを使うためにはAsyncHttpServerPluginインターフェースを実装する必要があります。
またこのプラグインではテンプレートは使用しないのでストラテジHttpBasicStrategyを使います。HttpBasicStrategyでプラグインを処理するにはHttpBasicPluginインターフェースを実装します。

 package chat.plugin;
 
 import java.util.List;
 import java.util.ArrayList;
 import java.io.IOException;
 
 import jp.maru.liquid.plugin.http.HttpRequest;
 import jp.maru.liquid.plugin.http.HttpResponse;
 import jp.maru.liquid.plugin.http.AsyncHttpEvent;
 import jp.maru.liquid.plugin.http.AsyncTrigger;
 import jp.maru.liquid.plugin.AsyncHttpServerPlugin;
 import jp.maru.liquid.plugin.HttpBasicPlugin;
 
 public class ChatPlugin implements AsyncHttpServerPlugin,HttpBasicPlugin
 {
     private List<AsyncTrigger> _triggerList=new ArrayList<AsyncTrigger>();
     private String _lastMessage;
     private String _bindURI;
 
     @Override
     public void init()
     {
     }
 
     @Override
     public void execute(HttpRequest request,HttpResponse response)
     throws IOException
     {
         response.setContentType("text/html; charset=UTF-8");
         response.getWriter().println(_lastMessage != null? _lastMessage: "");
     }
 
     @Override
     public void httpConnected(AsyncHttpEvent event,AsyncTrigger trigger)
     {
 
         _triggerList.add(trigger);
     }
 
     @Override
     public void httpDisconnected(AsyncHttpEvent event)
     {
     }
 
     public synchronized void message(String handle,String message)
     {
         try
         {
             _lastMessage=handle+": "+message;
 
             for(AsyncTrigger trigger : _triggerList)
                 trigger.resume();
             _triggerList.clear();
             _lastMessage=null;
         }
         catch(IOException ioe)
         {
             ioe.printStackTrace();
         }
     }
 
     @Override
     public String getBindURI()
     {
         return _bindURI;
     }
 
     public void setBindURI(String uri)
     {
         _bindURI=uri;
     }
 }

インターフェースAsyncHttpServerPluginには接続時にコールされるメソッドhttpConnected()、切断時にコールされるメソッドhttpDisconnected()が定義されているため、それぞれ実装を行います。
httpConnected()では接続を行ってきたクライアントのオブジェクトAsyncTriggerをプラグインChatPluginのインスタンス変数として用意したListに追加していく処理を行っています。今回はhttpDisconnected()では処理を行いません。
インターフェースHttpBasicPluginにはメソッドexecute()が定義されているのでこれを実装します。このメソッドではプラグインに渡されたチャットメッセージをクライアントへ送信する処理を行います。
メソッドmessage()では後述するActionクラスからの呼び出しに備えて、クライアントから送られてきたメッセージをプラグイン内のインスタンス変数へ保存し、Listへ登録されているクライアントに対しての送信処理を行っています。
この中のクラスAsyncTriggerのメソッドresume()により、一時停止していたクライアントからのリクエストを再開しクライアント毎にメソッドexecute()が呼び出されます。
プラグインはこれで完成です。次にクライアントからのメッセージを受け入れる処理をActionクラスに追加しましょう。

Actionクラスの修正

クライアントからはAjaxにより発言毎にハンドル、メッセージを受け取る仕様にするため、各々のパラメータを受信できるように変更します。LiquidのWebコンテナではパラメータをset〜というメソッドにより受け取るので、今回はsetHandle()、setMsg()というメソッドを作成します。
加えて発言の受信と同時に先に用意したChatPluginのメソッドmessage()に対して発言内容をポストするようにもしましょう。

 package chat.action;
 
 import jp.maru.liquid.plugin.Plugin;
 import jp.maru.liquid.core.plugins.http.annotation.Intercept;
 import jp.maru.liquid.core.plugins.http.annotation.InjectPlugin;
 import jp.maru.liquid.core.plugins.http.interceptor.PluginInterceptor;
 import jp.maru.liquid.core.plugins.http.interceptor.PluginAware;
 import chat.plugin.ChatPlugin;
 
 @Intercept(PluginInterceptor.class)
 @InjectPlugin(ChatPlugin.class)
 public class ChatAction implements PluginAware
 {
     private ChatPlugin _chatPlugin;
     private String _handle,_msg;
 
     public String index()
     {
         return "index.ftl";
     }
 
     public String jquery()
     {
         return "jquery.min.js";
     }
 
     public String msg()
     {
         _chatPlugin.message(_handle,_msg);
 
         return "dummy.ftl";
     }
 
 
     public void setHandle(String handle)
     {
         _handle=handle;
     }
 
     public void setMsg(String msg)
     {
         _msg=msg;
     }
 
     @Override
     public void setPlugin(Plugin[] plugins)
     {
         _chatPlugin=(ChatPlugin)plugins[0];
     }
 }

クラスに対してアノテーション@Intercept、@InjectPluginを設定しています。
アノテーション@InterceptはこのActionにリクエストが到着する前に処理されるインターセプタを指定します。今回はプラグインをインジェクトしたいので、PluginInterceptorを指定しています。
アノテーション@InjectPluginはインジェクトするプラグイン名を指定します。
これらのアノテーションによりインターセプタがAction内のsetPluginを通じて目的のプラグインをインジェクトし、Actionから直接プラグインへアクセスする事が可能になります。
Actionの修正はこれで完了です。次はテンプレートを修正しましょう。

テンプレートの修正

テンプレートではチャットの発言と発言履歴のロードが出来ればいいので、自由に作ってもらって構いません。
今回はjQueryを使いAjaxの通信部分を実装しています。jQueryのライブラリjquery.min.jsを/chat/wwwへ配置しましょう。

 <html>
 <head>
 <style type="text/css">
 <!--
 body > * {
     font-size:0.8em;
 }
 #chat {
     width:420px;
     height:250px;
     border:2px solid #FA5;
     overflow:auto;
     margin-top:10px;
     padding:3px;
 }
 
 #handle,#msg {
     border:2px solid #F75;
 }
 -->
 </style>
 </head>
 <body>
 <p>Liquid Sample - Comet Chat</p>
 
 <input type="text" id="handle" size="15">:<input type="text" id="msg" size="40">
 <br>
 
 <div id="chat"></div>
 
 <script type="text/javascript" src="/chat/Chat/jquery"></script>
 <script type="text/javascript">
 <!--
 $(document).ready(function()
 {
     $("#msg").keypress(checkReturn);
     waitResponse();
 });
 
 function sendMsg()
 {
     $.get("/chat/Chat/msg",{handle:$("#handle").val(),msg:$("#msg").val()});
     $("#msg").val("");
 }
 
 function waitResponse()
 {
     $.get("/chatComet/",function(response){
         if(response.length > 0)
             $("#chat").html(response+"<br>"+$("#chat").html());
         waitResponse();
     });
 }
 
 function checkReturn(event)
 {
     var keyCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
     if(keyCode==13)
         sendMsg();
 }
 
 //-->
 </script>
 </body>
 </html>

これでプラグラム類はとりあえず完了です。最後にplugin.xmlの修正をしましょう。

plugin.xmlの修正

先ほど作ったChatPluginをplugin.xmlへ登録します。
以下の内容に書き換えて下さい。

 <?xml version="1.0" encoding="euc-jp"?>
 <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
 <beans>
 
     <bean id="pluginManager" class="jp.maru.liquid.kernel.PluginManager">
         <property name="plugins" >
             <list>
                 <ref local="chatPlugin" />
                 <ref local="chatActionPlugin" />
             </list>
         </property>
     </bean>
 
     <bean id="chatPlugin"
      class="chat.plugin.ChatPlugin">
         <property name="bindURI" value="/chatComet/" />
     </bean>
 
     <bean id="chatActionPlugin"
      class="jp.maru.liquid.core.plugins.HttpActionPlugin">
         <property name="bindURI" value="/chat/*" />
         <property name="encoding" value="utf-8" />
         <property name="templatePath" value="/chat/www/" />
         <property name="actionClass" value="chat.action.*Action" />
     </bean>
 
 </beans>

コンパイル、実行

新たにクラスを作ったのでコンパイルを行います。/chat/libの中身をクラスパスに加えてコンパイルしましょう。
問題なくコンパイルが完了したら、再度run.shによってLiquidを起動させます。
http://xxx.xxx/chat/Chat/indexにアクセスしてチャットが出来れば成功です。

【動作デモ】
http://www.youtube.com/watch?v=Du11JiUmRmY

まとめ

Liquidは軽量なフレームワークですが高い拡張性を持っています。
機能をプラグインとして提供する事によりオブジェクト間の依存性を低くし、メンテナンス性が高く再利用可能なシステムの構築を目的として作られました。
今回は触れませんでしたが、他に単体実行可能なPluginをサポートするExecuteModule、永続化層のHibernateのサポート、アノテーションによるバリデーションなどの機能も搭載されています。
Webアプリケーションのみならずサーバサイドアプリケーションを構築するのにも適したフレームワークなので活用してもらえればと思います。
それではよいLiquidライフを。