ソート機能の実装

Ajaxを利用してソート機能を実装してみましょう。

以下のようにファイルを修正していきます。

変更対象 変更内容
struts.xml list_jsonアクションを追加する。
Repository.java,MockRepository.java,DBRepository.java queryItems(String keywords)メソッドに引数orderByを追加し、ソート機能を実装する。
ListAction.java,SearchAction.java パラメータにorder_byを追加し、ソート機能を実装する。
list.js ソート用スクリプトを実装する。
list.jsp,todo.css ソート用スクリプトを呼び出すためのボタンを追加する。


サーバプログラムの修正

JSON出力プラグインの追加とlist_jsonアクションの追加

サーバの処理にlist_jsonアクションを追加します。

Struts2には、JSON出力を簡単に実現できるプラグインが存在します。
ここでは、プラグインを利用し、アプリケーションにJSON出力機能を付加します。
また、ListAction.javaにソート用のパラメータおよび処理を追加します。

  1. 以下のファイルをダウンロードし、WEB-INF/libにコピーしてください。

    ダウンロード jsonplugin-0.33.jar


  2. struts2todoプロジェクトを右クリックし「Properties」を選択し、「Java Build Path」に WEB-INF/lib以下のJARファイルを設定してください。

    これで、JavaプログラムからJARファイル内にあるクラスを参照可能になります。

  3. WEB-INF/src/struts.xmlを以下のように修正します。
    ここでは、ブラウザからの要求に応じてソートした作業一覧をJSONで送信することを設定します。

    ダウンロード struts.xml
    11行目:
     11: <package name="todo" extends="json-default">

    JSONパッケージを利用するために、パッケージをjson-defaultパッケージをベースとしたパッケージに変更。
    この宣言により、パッケージ内でJSON出力機能を利用することができるようになります。
    24~29行目:
     24: <action name="list_json" class="test.ListAction" method="show">
     25:     <result type="json">
     26:         <param name="excludeProperties">.*\.user\.password,currentUser\.password</param>
     27:     </result>
     28:     <result name="error">/WEB-INF/jsp/error.jsp</result>
     29: </action>

    JSON出力をおこなうアクションlist_jsonの宣言。
    result要素のtype属性に"json"を指定することで、 アクションの持つgetterメソッドから取得可能なプロパティを自動的にJSONデータ化することができます。
    ここでは、セキュリティの観点から、パスワード情報はJSONデータに含めないように宣言しています。


Javaクラスの修正

Javaクラスに対してソート機能を追加していきます。

  1. Repositoryインタフェースおよびその実装クラスにソート機能を実装します。
    アイテムリストを取得するためのメソッドItem[] queryItems(String keywords)に対してソート用のカラム名を設定します。
    以下のようにコードを修正してください。

    ダウンロード Repository.java
    48行目:
     48: public Item[] queryItems(String keywords, String orderBy);

    queryItemsメソッドに引数orderByを追加。

    ダウンロード MockRepository.java
    114~130行目:
    114: public Item[] queryItems(String keywords, String orderBy) {
    115:     ArrayList<Item> items = getItems();
    116:     if(keywords != null) {
    117:         ArrayList<Item> result = new ArrayList<Item>();
    118:         for(int i = 0; i < items.size(); i ++) {
    119:             Item item = items.get(i);
    120:             if(item.getName().contains(keywords)) {
    121:                 result.add(item);
    122:             }else if(item.getUser().getName().contains(keywords)) {
    123:                 result.add(item);
    124:             }
    125:         }
    126:         return result.toArray(new Item[0]);
    127:     }else{
    128:         return items.toArray(new Item[0]);
    129:     }
    130: }

    queryItemsメソッドに引数orderByを追加。
    MockRepositoryクラスでは、orderByが指定されても特に処理はおこなわないものとします。

    ダウンロード DBRepository.java
    139行目:
    139: public Item[] queryItems(String keywords, String orderBy) {

    queryItemsメソッドに引数orderByを追加。
    168~189行目:
    168: if(orderBy != null) {
    169:     StringTokenizer tokenizer = new StringTokenizer(orderBy, " \t");
    170:     StringBuffer orderByBuf = new StringBuffer();
    171:     while(tokenizer.hasMoreTokens()) {
    172:         String token = tokenizer.nextToken();
    173:         if(orderByBuf.length() > 0) {
    174:             orderByBuf.append(", ");
    175:         }
    176:         String dir = "ASC";
    177:         String name = token;
    178:         if(token.startsWith("!")) {
    179:             dir = "DESC";
    180:             name = token.substring(1);
    181:         }
    182:         orderByBuf.append("item.");
    183:         orderByBuf.append(name);
    184:         orderByBuf.append(" ");
    185:         orderByBuf.append(dir);
    186:     }
    187:     
    188:     ql += " ORDER BY " + orderByBuf.toString();
    189: }

    引数orderByが指定されたときの処理。
    orderByが指定された場合、JPQL文字列内にORDER BYを加え、クエリ発行時にソートするようにします。


  2. アクションクラスを修正します。以下のようにコードを修正してください。

    ダウンロード ListAction.java
    25行目:
     25: private String _orderBy;

    orderByプロパティの宣言。
    51~65行目:
     51: /**
     52:  * ソートキーを保持します。
     53:  * @param orderBy
     54:  */
     55: public void setOrder_by(String orderBy) {
     56:     _orderBy = orderBy;
     57: }
     58: 
     59: /**
     60:  * ソートキーを取得します。
     61:  * @return
     62:  */
     63: public String getOrder_by() {
     64:     return _orderBy;
     65: }

    orderByプロパティのsetter,getterメソッドの宣言。
    82~85行目:
     82: @Override
     83: public User getCurrentUser() {
     84:     return super.getCurrentUser();
     85: }

    currentUserプロパティの宣言。
    JSON化する際にListActionクラスで宣言された全getterからオブジェクトが取得されます。
    91行目:
     91: _items = _repository.queryItems(null, _orderBy);

    queryItemsメソッド呼び出しの修正。

    ※SearchActionクラスも同様の修正を実施します。

    ダウンロード SearchAction.java


クライアントの修正

ブラウザ側でユーザの入力を受けつけ、処理をおこなうJavaScriptプログラムを一覧画面に追加します。
JavaScriptで記述する処理のうち、主要なものは以下のとおりです。

種別 記述 説明
prototype.js $("ID") IDで指定されたNodeを取得する。
(JavaScriptの基本で説明したdocument.getElementByIdと同様の機能)
Nodeに対して可能な操作はDOMを参照。
Ajax.Request("URL", {method: "メソッド", parameters: "クエリ", onSuccess: 成功時の関数, onFailure: 失敗時の関数}) URLに対して、メソッド、クエリで指定されたリクエストを発行する。
(この関数は内部でXMLHttpRequestを呼び出しており、
ブラウザ間の互換性を考慮して実装されています。)
ステータスコードが200(OK)の場合はonSuccessで指定された関数が、
それ以外の場合はonFailureで指定された関数が呼び出される。

onSuccessの第一引数.responseText.evalJSON()とすることでJSONオブジェクトが取得可能。
DOM (Node).tagName Nodeが要素(タグ)である場合にタグ名を取得する。
Nodeがタグではない(テキストやコメントなど)の場合はundefinedとなる。
(Node).childNodes Nodeの子であるNodeの一覧を取得する。
戻り値に対してlength(Node数を取得), [(インデックス)](インデックスに対応するNodeを取得)を使用することで、 Nodeの一覧を処理することができる。
(Node).hasAttribute("属性名") Nodeに属性が指定されているかどうかを判定する。
属性とはのnameとvalueの組を指す。
(Node).getAttribute("属性名") Nodeに指定されている属性値を取得する。
(Node).style.(スタイルプロパティ名) = (スタイルプロパティ値) Nodeにスタイルを指定する。
プロパティ名、プロパティ値にはCSSで説明したものが指定できる。
Nodeが要素である場合に有効。
(Node).data Nodeにテキストデータを指定する。
Nodeがテキストである場合に有効。
  1. 以下のファイルをダウンロードし、struts2todoプロジェクト以下にコピーしてください。

    ダウンロード prototype.js


  2. ブラウザ部分のプログラムを作成します。以下のようなコードを作成してください。

    ダウンロード list.jsp
    8~11行目:
      8: <script type="text/javascript" src="prototype.js">
      9: </script>
     10: <script type="text/javascript" src="list.js">
     11: </script>

    ロードするJavaScriptファイルの定義。
    前の手順で配置したprototype.jsとこれから作成するlist.jsを配置します。
    46行目:
     46: <table border="0" width="90%" class="list" id="item_list">

    テーブルに関するID定義。
    スクリプトによるテーブルの操作は、このIDを介しておこないます。
    48~59行目:
     48: <th>
     49:     項目名<span onclick="updateList('name');" id="sort_name_a" class="sort_button"
    onmouseover="mouseOver('name');" onmouseout="mouseOut('name');">▼</span>
    <span onclick="updateList('!name');" id="sort_name_d" class="sort_button"
    onmouseover="mouseOver('!name');" onmouseout="mouseOut('!name');">▲</span> 50: </th> 51: <th> 52: 担当者<span onclick="updateList('user');" id="sort_user_a" class="sort_button"
    onmouseover="mouseOver('user');" onmouseout="mouseOut('user');">▼</span>
    <span onclick="updateList('!user');" id="sort_user_d" class="sort_button"
    onmouseover="mouseOver('!user');" onmouseout="mouseOut('!user');">▲</span> 53: </th> 54: <th> 55: 期限<span onclick="updateList('expireDate');" id="sort_expire_a" class="sort_button"
    onmouseover="mouseOver('expireDate');" onmouseout="mouseOut('expireDate');">▼</span>
    <span onclick="updateList('!expireDate');" id="sort_expire_d" class="sort_button"
    onmouseover="mouseOver('!expireDate');" onmouseout="mouseOut('!expireDate');">▲</span> 56: </th> 57: <th> 58: 完了<span onclick="updateList('finishedDate');" id="sort_finished_a" class="sort_button"
    onmouseover="mouseOver('finishedDate');" onmouseout="mouseOut('finishedDate');">▼</span>
    <span onclick="updateList('!finishedDate');" id="sort_finished_d" class="sort_button"
    onmouseover="mouseOver('!finishedDate');" onmouseout="mouseOut('!finishedDate');">▲</span> 59: </th>

    ソートボタンの定義。
    スタイルシートによりカーソル形状を定義し、マウス移動、クリック時にスクリプトの関数を呼び出すように設定しています。

    ダウンロード list.js
    8~29行目:
      8: function updateList(key)
      9: {
     10:     var msec = (new Date()).getTime();
     11:     new Ajax.Request("list_json.action", {
     12:         method: "post",
     13:         parameters: "cache="+msec + "&order_by=" + encodeURIComponent(key),
     14:         onSuccess:function(httpObj){
     15:             try{
     16:                 var obj = httpObj.responseText.evalJSON();
     17:                 
     18:                 var updater = new Updater(obj.currentUser, obj.items, obj.order_by);
     19:                 updater.update();
     20:                 
     21:             }catch(e) {
     22:                 alert(e);
     23:             }
     24:         },
     25:         onFailure:function(httpObj){
     26:             alert("HTTP読み込みエラー");
     27:         }
     28:     });
     29: }

    サーバに対するリクエスト・レスポンス処理。
    prototype.jsのAjaxRequest関数を利用しています。
    成功時にはレスポンスをJSONとみなしオブジェクトに変換し、Updaterオブジェクトのupdate関数を呼び出します。
    104~175行目:
    104: this.update = function()
    105: {
    106:     var trCount = this._getTRElementCount();
    107:     
    108:     if(trCount - 1 != this._items.length) {
    109:         var option = "";
    110:         if(this._orderBy != undefined) {
    111:             option = "?order_by=" + encodeURIComponent(this._orderBy);
    112:         }
    113:         window.location.href = "list.action" + option;
    114:         return;
    115:     }
    116:     
    117:     var currentDate = this._getCurrentDate();
    118:     for(var i = 0; i < this._items.length; i ++) {
    119:         var item = this._items[i];
    120:         
    121:         var nameText = this._getText(i,0);
    122:         if(nameText != undefined) {
    123:             nameText.data = item.name;
    124:         }
    125:         var userText = this._getText(i,1);
    126:         if(userText != undefined) {
    127:             userText.data = item.user.name;
    128:         }
    129:         var expireText = this._getText(i,2);
    130:         var tdStyle = "";
    131:         var colorStyle = "#000000";
    132:         var bgColorStyle = "#bbbbff";
    133:         var expireString = this._getDate(item.expireDate);
    134:         var expireDate = this._parseDate(expireString);
    135:         if(expireText != undefined) {
    136:             expireText.data = expireString;
    137:         }
    138:         var finishedString = undefined;
    139:         var finishedText = this._getText(i,3);
    140:         if(finishedText != undefined) {
    141:             if(item.finishedDate == null) {
    142:                 finishedText.data = "未";
    143:                 if(currentDate.getTime() > expireDate.getTime()) {
    144:                     colorStyle = "#ff0000";
    145:                 }
    146:                 if(this._currentUser.id == item.user.id) {
    147:                     bgColorStyle = "#ffbbbb";
    148:                 }
    149:             }else{
    150:                 finishedString = this._getDate(item.finishedDate);
    151:                 finishedText.data = finishedString;
    152:                 bgColorStyle = "#cccccc";
    153:             }
    154:         }
    155:         
    156:         for(var j = 4; j < 7; j ++) {
    157:             var hiddenElem = this._getHiddenElement(i, j);
    158:             hiddenElem.setAttribute("value", item.id);
    159:         }
    160:         
    161:         var submitElem = this._getSubmitElement(i, 4);
    162:         if(item.finishedDate == null) {
    163:             submitElem.setAttribute("value", "完了");
    164:         }else{
    165:             submitElem.setAttribute("value", "未完了");
    166:         }
    167:         
    168:         for(var j = 0; j < 7; j ++) {
    169:             var divElem = this._getDIVElement(i, j);
    170:             divElem.style.color = colorStyle;
    171:             var tdElem = this._getTDElement(i, j);
    172:             tdElem.style.backgroundColor = bgColorStyle;
    173:         }
    174:     }
    175: };

    レスポンスのDOMへの反映処理。
    レスポンスの解析によって作成されたオブジェクトの値をテーブルの各要素へと反映していきます。

    ダウンロード todo.css
    80~82行目:
     80: span.sort_button {
     81:     cursor: default;
     82: }

    ソートボタンに関するスタイル宣言。
    cursorプロパティを定義することで、オブジェクト上にマウスポインタがある場合のカーソル形状をカスタマイズすることが可能です。


ここまでのファイル

ここまででプロジェクトディレクトリの内容は以下のようになります。
正しく作成できているか、確認してください。

ダウンロード struts2todo_1.zip


デバッグ

ブラウザで以下のURLを開きます。
一覧画面中の各カラムのソートボタンを押したときに、テーブルの順序が入れ替われば成功です。

http://localhost:8080/struts2todo/login.action