ちょっと Web サイトの見直しのために,データベース検索の日曜大工をしているところである。その過程で,最近流行の jQuery JavaScript ライブラリで遊んだりもしている。jQuery は動的な Web サイトを構築するに際していまやなくてはならないプラットフォームになっていて,これを使えば少ないコードで簡便に,コンテンツを書換えたり,アニメーションを付加したりできる。jQuery はバージョンアップで古いコードが動かなくなることがあるのが悩ましいのだが。
データベース検索というと,キーワード等の検索条件に合致する検索結果を表示するにあたり,一定の件数を表示するとともに,前後ページ,あるいは複数ページへのリンクを示してナビゲートするのが一般的インタフェースといえる(ネクストページ・インタフェースというそうである)。私の今回の日曜大工の課題は,自分なりにこの仕組みを構築してみることだった。JavaScript でクエリをサーバに送信し,Java Servlet でデータベースを検索し,その応答を再度 JavaScript で整形して HTML として表示する。ここで JavaScript と Java Servlet のデータインタフェースを JSON(JavaScript Object Notation)データ記述言語に準拠することにした。
大規模システムではシステム間通信のデータ記述言語として XML を選択することが多いと思われる。これは,よく言われるとおり,XML は「自己説明的」(誰もがこれを口にするが,何がメリットなのかよくわからない。ま,要するに,見て意味がわかりやすいということ)で,大人数を擁するプロジェクトで共有する基盤として適当であるし,また SAX や Xerces などの便利な XML パーサーがたくさん転がっているためである。一方,JSON はその名のとおり(十三日の金曜日ではなく)
jQuery には ajax メソッドが用意されている。つまり,かつて XMLHttpRequest なり ActiveXObject なりの低レベルの(「おつむが足りない」ということではなくて,よりコンピュータの作りに近い,従って面倒な)オブジェクトを直接操作していたのに比べると,ブラウザの種類に悩まされず比較的簡便に,サーバとの非同期通信コードを書くことができるようになった。今回,これを使わない手はない。
前置きが長くなるのがボクの悪いクセ(アホか)。ある著名な俳句・短歌専門出版社の書籍データを用いて書籍検索システム(ネクストページ・インタフェース)を構築してみた。サーバ側は,Java Servlet on Tomcat, FreeBSD 8。データベース・マネージャは SQLite3 である。ちょっと使ってみていただきたい(そのうち撤去するかも知れないが)。注文カートに入れるボタンも付けてある。もちろん架空のインターネットショッピング・サイトのイメージである。Cookie を食わせるので,注意いただきたい。
もし上のフレームでうまく動かないようなら(ドメイン絡みで Cookie がうまく食わせられない),http:
さて,上記検索プログラムの構築について,簡単にメモを残しておく。前置きが長くなるのがボクの悪いクセ(アホか)。
ネクストページ実現方式
ネクストページの実現は,一発目の検索で一ページに表示すべき明細とともにヒット件数を取得し,ページ当りの表示件数とそこから計算できるオフセット数値をもとに,前後ページのリンクを設置して行けばよい。つまり,仮にページ当りの表示件数が 10 件で,検索ヒット件数が 32 件だとすると,現在ページが 1 ページなら 2 ページ目のリンクは 11 件目から 10 件を,4 ページ目は 31 件目から 2 件を表示すべきリンクとなる。このリンクにデータベース検索における limit 10 offset N (N は 開始のオフセット) クエリを埋め込むようにすればよいわけである。検索種別(一発目の件数取得をする: 1 か,否: 0 か),オフセット(言わずもがなであるが,offset クエリは 0 から始まるので,21 件目のオフセットは 20 である)を引数に取ってサーバ・データベースにクエリを出す JavaScript 関数(bookSearch
<a class="pglink" href="javascript:void(0)" onClick="bookSearch(0, 10);">2</a> 3 (現在ページ) <a class="pglink" href="javascript:void(0)" onClick="bookSearch(0, 30);">4</a> ...
bookSearch(0, 30) を受けて,キーワード「短歌」で 31 件目から 6 件分を取って来る SQL は次のようになる。 textdt に書籍のテキストデータをまとめて放り込んであり,「短歌」の含まれるレコードを like 文で探索する。
select * from BOOKTBL where textdt like '%短歌%' limit 6 offset 30;
DB 検索 Servlet Java コード
サーバ側 JavaServlet のコードを示しておく。これは完全なコードで,このままでコンパイルが通るはずである。一回目の検索だけ(kind パラメータが 1 のとき)select count(*) でヒット件数を取得するようになっている。JSON データの構築に JsonGenerator クラス(javax.
// -*- coding: utf-8; mode: java; -*- import java.io.*; import java.net.*; import java.util.regex.*; import javax.servlet.*; import javax.servlet.http.*; import java.sql.*; import javax.json.*; import javax.json.stream.*; import org.apache.log4j.Logger; /** * <pre> * BookSearch 書籍検索 JSONデータインタフェース * Copyright(c) 2013, isao yasuda, All Rights Reserved. * </pre> * @author 安田 功 (Isao YASUDA) * @version $Id: insomnia.txt 222 2014-03-27 11:23:12Z isao $ */ public class BookSearch extends HttpServlet { /** SQLite3 DB path */ private static String dbpath = "/usr/local/etc/BOOK.db"; /** SQL */ private String query = null; /** 許容ドメイン */ private static String authsite = "yasuda.homeip.net"; /** ドメインチェック用正規表現パターン */ private static Pattern dmptn; /** デバッグモード */ private static Boolean debug = false; /** Log4j Logger */ private static Logger log = Logger.getLogger(BookSearch.class.getName()); /** 初期化 */ public void init() throws ServletException { // デプロイメントディスクリプタから初期パラメータを取得する. String p = null; p = getInitParameter("dbpath"); if (p != null) dbpath = p; p = getInitParameter("debug"); if (p != null) if (p.equals("true")) debug = true; p = getInitParameter("authsite"); if (p != null) authsite = p; dmptn = Pattern.compile(authsite); // 開始メッセージ (log4j) log.info("\nBook Search initialization." + "\n- SQLite3 Book Database file:: " + dbpath + "\n- Search authorized site: " + authsite + "\n- Debug mode: " + debug); } /** リクエスト処理 */ public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { /** ブラウザ情報の取得 */ String ip = req.getRemoteAddr(); // IP アドレス String ua = req.getHeader("User-Agent"); // User Agent String rf = req.getHeader("Referer"); // Referer log.info("Client: " + ip + ", " + ua + ", " + rf); /** リファラドメインチェック */ Matcher dm = dmptn.matcher(rf); if (dm.find()) { if (debug) log.info(ip + ", " + rf + " authorized."); } else { log.info(ip + ", " + rf + " not authorized."); return; // 許可されていないページからの要求は無視 } /** DB検索リクエストパラメータ */ String cond = (String) req.getParameter("query"); String qkind = (String) req.getParameter("kind"); if (debug) log.info(ip + ", " + ua + ", " + rf + " query: " + cond + "; kind: " + qkind + ";"); /** DB検索用変数 */ Connection conn = null; Statement stmt = null; String query = null; String count = null; /** JSON */ StringWriter wrtr = new StringWriter(); JsonGenerator generator = Json.createGenerator(wrtr); try { // SQLite3 JDBC コネクション Class.forName("org.sqlite.JDBC"); conn = DriverManager.getConnection("jdbc:sqlite:" + dbpath); conn.setAutoCommit(false); stmt = conn.createStatement(); stmt.setQueryTimeout(10); // set timeout to 10 sec. // 検索結果セット ResultSet rset = null; // JSON Array generator.writeStartArray(); // JSON Array 開始 '[' // 1回目のみ結果件数を取得 if (qkind.equals("1")) { query = "select count(*) as cnt from BOOKTBL where " + cond + ";"; rset = stmt.executeQuery(query); count = rset.getString("cnt"); // ヒット件数 generator.writeStartObject(). // count JSONオブジェクト '{' write("count", count). // count: ヒット件数 writeEnd(); // count 終了 '}' } // 検索実行 query = "select * from BOOKTBL where " + cond + ";"; rset = stmt.executeQuery(query); while (rset.next()) { // ResultSetをJSONオブジェクトにセット generator.writeStartObject(). // DB明細JSONオブジェクト開始 '{' write("kind", rset.getString("kind")). // カテゴリー write("image", rset.getString("image")). // カバー画像 write("price", rset.getString("price")). // 価格 write("pinfo", rset.getString("pinfo")). // 新刊等情報 write("title", rset.getString("title")). // 題名 write("subttl", rset.getString("subttl")). // 副題 write("author", rset.getString("author")). // 著者名 write("pubdate", rset.getString("pubdate")).// 刊行日 write("isbn", rset.getString("isbn")). // ISBN write("comment", rset.getString("comment")).// コメント writeEnd(); // 明細1件終了 '}' } generator.writeEnd(); // JSON Array を閉じる ']' generator.close(); // JSON Generator を閉じる rset.close(); stmt.close(); conn.close(); } catch (Exception e) { log.error("BookSearch Error: " + e.getMessage()); } /** 結果出力 */ res.setContentType("application/json; charset=UTF-8"); res.setHeader("Cache-Control", "no-cache"); res.getWriter().write(wrtr.toString()); log.info(ip + ", " + ua + ", " + rf + ", " + "\nhits: " + count + ", Input: " + cond); } /** 終了 */ public void destroy() { log.info("Book Search Terminate."); } }
SQLite3 データベースは,某俳句・短歌出版社のサイトから Web ページをダウンロードし,Perl HTML::
クライアントに返すデータを [ { "項目1": "val11", "項目2": "val12", ... }, { "項目1": "val21", "項目2": "val22", ... }, ... ] というように JSON 配列様式に整形しているところがポイントである。setContentType にも application/
コンパイルには Java Servlet 関係のクラスライブラリのほか,JSONP (Java API for JSON Processing) JSR 353: 1.0.4(Java EE 標準に組込まれようとしている最中で,頻繁に仕様が変るのが悩ましい),Log4j 1.2,SQLite3-JDBC が必要である。
JavaScript コード
クライアント,すなわちブラウザで動作する JavaScript の主要コードは以下のとおりである。モーダルウィンドウ表示,オーダーなど,こまごました付随的な処理を記述した下請け関数は割愛してある(だから,このまま掲載コードをコピペしただけでは動きません)。メインは,ネクストページリンクとなる bookSearch 関数,SQL クエリを組立てる makeQuery 関数,サーバに非同期通信要求を出す invokeServer 関数,検索結果明細を表示する editResult 関数,ページリンクを組立てる makePageLink 関数である。invokeServer 関数のなかで,jQuery $.ajax を使い,さらにこれに対し JSON インタフェース(dataType: "json")を指示することにより,データを受けとった段階ですぐさま JavaScript 配列としてアクセスできるようにしている。jQuery ajax は非同期通信の処理(成功時,エラー時,後始末)を簡潔に書くことができる。jQuery を使うようになったいま,かつて getElementById なんかでちまちま HTML 要素を操作していたのが懐かしくなる。
// -*- coding: utf-8; mode: javascript; -*- // BookSearch 書籍検索 // 2013(c) isao yasuda, All Rights Reserved. // $Id: insomnia.txt 222 2014-03-27 11:23:12Z isao $ // Global Valuables var skind = 1; // 検索種別: 最初の検索時またはヒット0のとき1 var sptext = null; // 検索結果表示先頭に挿入するsnippet var squery = null; // SQL where句 parts var scount = null; // hit 件数 var skword = null; // 検索キーワード var sgenre = null; // 検索対象ジャンル var ssitem = null; // 新刊等の属性項目 var sprmin = null; // 価格下限値 var sprmax = null; // 価格上限値 var slimit = 12; // 表示件数デフォルト var sofset = 0; // オフセット var bookid = 0; // 書籍ID(注文用) var explimit = 6; // cookie expire hours // サーバに検索リクエストを POST する function invokeServer() { // SQL where句のクエリーを整形する makeQuery(); var kind = "0"; if (skind) kind = "1"; // 検索種別 "1": ヒット件数付き // Ajax Communication to receive JSON data $.ajax({ url: "http://yasuda.homeip.net/books/search", // JavaServlet App URL type: "POST", dataType: "json", cache: false, data: { "query": squery + ' limit ' + slimit + ' offset ' + sofset, "kind": kind } }).done(function(json, textStatus, jqXHR) { // 通信成功 editResult(json); }).error(function(jqXHR, textStatus, errorThrown) { // 通信失敗 errorAlert("通信エラー", "status: " + textStatus + "\nthrown: " + errorThrown); }).complete(function(jqXHR, textStatus) { // 通信完了 window.status = "BookSearch completed, textStatus: " + textStatus; }); } // Book Search // - 指定検索種別とオフセット値で検索を実行する function bookSearch(flag, offset) { sofset = offset; // offset 値設定 skind = flag; // 検索種別設定 if (skind) { // true なら新規検索 var cflg = false; // 条件入力フラグ // 検索結果の前に挿入するテキスト sptext = '<h2>検索書籍</h2>'; // 検索キーワード抽出 var maxc = 100; // キーワード長最大値(とりあえず) skword = $("#search-basic").val(); if (skword.length > maxc) { errorAlert("検索条件エラー", "入力の文字数が" + maxc + "を越えています。それ以下にしてください"); return; } else if (skword.match(/^\s+$/) || skword.length == 0) { } else cflg = true; // 対象ジャンル sgenre = $("#genre").val(); if (sgenre != 0) cflg = true; // 新刊 ssitem = $("#sitem").prop("checked"); // 検索対象項目 if (ssitem) { ssitem = 1; cflg = true; } else ssitem = null; // 価格範囲 check and set var num = $("#prmin").val(); // 価格下限 if (num.match(/^[0-9]+/)) { sprmin = num; cflg = true; } else { if ((num != null) && (num.match(/[^0-9]+/))) { errorAlert("検索条件エラー", "価格下限値には数値(半角)以外指定できません"); return; } sprmin = null; } num = $("#prmax").val(); // 価格上限 if (num.match(/^[0-9]+/)) { sprmax = num; cflg = true; } else { if ((num != null) && (num.match(/[^0-9]+/))) { errorAlert("検索条件エラー", "価格上限値には数値(半角)以外指定できません"); return; } sprmax = null; } // 検索実行要件チェック if (!cflg) { errorAlert("検索条件エラー", "検索実行にはキーワード,..."); return; } // limit check and set num = $("#limit").val(); // 表示個数 if (num.match(/^[0-9]+/)) { if ((num < 5) || (num > 50)) { errorAlert("表示個数範囲エラー", "表示個数は 5 以上 50 以下でなければなりません"); return; } slimit = num - 0; } else { if ((num != null) && (num.match(/[^0-9]+/))) { errorAlert("条件設定エラー", "表示個数には数値(半角)以外指定できません"); return; } slimit = 6; // default } } // サーバ実行要求を出す invokeServer(); } // SQL where句条件を組み立てる function makeQuery() { squery = ""; // クエリ(where句以下) var andflg = false; // and で連結すべきかどうか true: する; false: しない // keywords if ((skword != "") && (skword != null)) { if (skword.match(/\s+/)) { // 空白文字区切りcheck var words = skword.split(/\s+/); var len = words.length - 1; // 最後の項目は null なので無視 if (len > 0) { // キーワード複数の場合 for (var i = 0; i < len; i++) { if (words[i] != "") squery += 'textdt like \'%' + words[i] + '%\' and '; } if (words[len] == "") squery = squery.replace(/ and $/, ""); else squery += 'textdt like \'%' + words[len] + '%\'' ; } else // キーワード1個 squery = 'textdt like \'%' + words[len] + '%\'' ; } else squery = 'textdt like \'%' + skword + '%\'' ; andflg = true; } // price 下限値 if (sprmin != null) { if (andflg) { squery += ' and price >= ' + sprmin; } else { squery += 'price >= ' + sprmin; andflg = true; } } // price 上限値 if (sprmax != null) { if (andflg) { squery += ' and price <= ' + sprmax; } else { squery += 'price <= ' + sprmax; andflg = true; } } // genre: sgenre には DB kind に 1 加えた値が入っている if (sgenre != 0) { if (andflg) { squery += ' and kind=' + (sgenre - 1); } else { squery += 'kind=' + (sgenre - 1); andflg = true; } } // NEW 書籍 if (ssitem == 1) { if (andflg) { squery += ' and pinfo like \'%NEW%\''; } else { squery += 'pinfo like \'%NEW%\''; andflg = true; } } } // 検索結果 JSON データ編集 // - 回答電文を編集し HTML に出力する function editResult(json) { var edit = ""; // 整形後のHTMLブックsnippet if (sptext != null) edit += sptext; // 先頭挿入snippet // 検索結果の取り出し for (var i = 0; i < json.length; i++) { if (json[i].count != undefined) { // ヒット件数レコードの場合 // ヒット件数 scount = parseInt(json[i].count); // ヒット件数表示 if (i == 0) edit += dispHit(); continue; } // ヒット件数表示 if (i == 0) edit += dispHit(); // 書籍情報表示 /* DB Schema 0: kind 種別 0:俳句; 1:短歌; 2:その他 1: image 画像ファイル名 2: price 税込価格 3: pinfo 付加情報 4: title タイトル 5: subttl サブタイトル 6: author 著者名 7: pubdate 刊行日 8: isbn ISBN 9: comment コメント */ // 左側: カバー画像 edit += '<div class="book_frame">' + // カバー画像 '<div class="book_left"><div class="book_img_frm">' + '<img class="book_img" src="./books/' + json[i].image + '" />' + '</div><div class="book_pinfo">'; // 新刊等の情報表示 var newsign = '<span class="book_info">'; var pinfo = json[i].pinfo; if (pinfo != "") { if (pinfo.match(/NEW/i)) { pinfo = pinfo.replace(/[\s\W]*NEW[\s\W]*/i, ""); newsign += pinfo + '</span> <span class="book_new">NEW</span>'; } else newsign += pinfo + '</span>'; } else newsign = ""; edit += newsign + '</div></div>'; // book_left end // 右側: 題名,著者名,価格,注文ボタン,刊行日,ISBN,コメント edit += '<div class="book_right"><div class="book_title">' + json[i].title + catSubTtl(json[i].subttl) + '</div>' + '<div class="book_author">' + json[i].author + '</div>' + '<div class="book_price"><div class="book_yen">¥ ' + insertComma(json[i].price) + ' (税込価格)</div>' + makeBuySnip(json[i]) + '</div>' + '<div class="book_pdate">' + json[i].pubdate + '</div>' + '<div class="book_isbn">' + json[i].isbn + '</div>' + '<div class="book_comm">' + json[i].comment + '</div>'; edit += '</div></div>'; // book_right, book_frame end } // Prev/Nextページリンク表示 edit = edit.replace(/&#/g, "&#"); // 数値参照 edit += makePageLink(); // 編集結果を挿入 $("#search_result").html(edit); // 先頭に遷移 (jQuery + easing) $('body,html').animate({ scrollTop: 0 }, { duration: 1000, easing: 'easeInOutCubic' }); } // Prev/Next ページリンクを表示する // - 最大,前後 5 ページ分,総 11 ページ分のページリンクを作成 // - 中央に現在ページをリンクなしで表示 // - prev / next を表示し前後ページリンクを付加する // <div class="search_link"> // <div class="search_prev">Prev « [6のリンク] </div> // <div class="sprev">2[リンク付き]</div> [3 4 5 6 繰返] // <div class="scurt">7</div> // <div class="snext">8[リンク付き]</div> [9 10 11 12 繰返] // <div class="search_next"> » Next[8のリンク]</div> // </div> // - リンク: // <a href="javascript:void(0);" onClick="bookSearch(0, offset);">no</a> function makePageLink() { var snip = '<div class="search_link">'; // 前リンク配列構築 var wofset = sofset; var plist = new Array(); for (var i = 0; i < 5; i++) { wofset -= slimit; if (wofset >= 0) plist[i] = wofset; else plist[i] = null; } // 後リンク配列構築 wofset = sofset; var nlist = new Array(); for (var i = 0; i < 5; i++) { wofset += slimit; if (wofset > scount) nlist[i] = null; else nlist[i] = wofset; } // 配列から前リンク生成 snip += '<div class="search_prev">' + makeAnchor(plist[0], "Prev « ") + '</div>'; for (var i = 4; i >= 0; i--) snip += '<div class="sprev">' + makeAnchor(plist[i], null) + '</div>'; // 現在ページ var pcur = (sofset / slimit) + 1; snip += '<div class="scurt">' + pcur + '</div>'; // 配列から後リンク生成 for (var i = 0; i < 5; i++) snip += '<div class="snext">' + makeAnchor(nlist[i], null) + '</div>'; snip += '<div class="search_next">' + makeAnchor(nlist[0], " » Next") + '</div></div>'; return snip; } // ページアンカー生成 // - offset: DB検索の offset; ltext: アンカーテキスト function makeAnchor(offset, ltext) { var atext = ""; if (offset != null) { var page = (offset / slimit) + 1; atext += '<a href="javascript:void(0);" onClick="bookSearch(0, ' + offset + ');" title="(0, ' + offset + ')">'; if (ltext != null) atext += ltext + '</a>'; else atext += page + '</a>'; } else { if (ltext != null) atext = ltext; else atext = " "; } return atext; } // ページ件数表示 function dispHit() { if (scount == 0) return '<div class="book_count">条件に合致する書籍はありませんでした。</div>'; else return '<div class="book_count"><span>' + scount + '点あります。そのうち' + (sofset + 1) + '点目から' + calcEnd() + '点目を表示しています。</span></div>'; } // ページ最終案件数を計算する function calcEnd() { if ((sofset + slimit) >= scount) // 最後のページ return scount; // ヒット件数 else return (sofset + slimit); // オフセットから表示件数 }
BookSearch デプロイメントデスクリプタ
Java Servlet アプリケーションを Tomcat7 に配備するためのデプロイメントデスクリプタ web.xml を以下に示す。
ここで,dbpath, authsite という初期設定パラメータは,BookSearch アプリケーション独自の設定である。前者は SQLite3 書籍データベースのパス,後者はアプリケーション要求を受け付けるドメインの正規表現指定になっている。後者は特定サイト以外のページからの利用ができないようにするための工夫である(完全ではない)。
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd"> <!-- -*- coding: utf-8; mode: xml; -*- --> <!-- BookSearch web.xml デプロイメントデスクリプタ Copyright (c) 2013, isao yasuda, All Rigths Reserved. $Id: web.xml 1 2013-12-20 17:01:27Z isao $ --> <web-app> <!-- 説明 --> <display-name>二日酔書店 Web アプリケーション</display-name> <description>二日酔書店書籍検索</description> <!-- BookSearch サーブレット --> <servlet> <servlet-name>BookSearch</servlet-name> <servlet-class>BookSearch</servlet-class> <load-on-startup>1</load-on-startup> <!-- 初期設定パラメータ --> <!-- dbpath DBパス --> <init-param> <param-name>dbpath</param-name> <param-value>/usr/local/tomcat7/webapps/books/WEB-INF/classes/BOOK.db</param-value> </init-param> <!-- authsite 実行許可サイト --> <!-- - yasuda.homeip.net, beatrice: 本番機 --> <!-- - margarita, isolde: 開発機 --> <init-param> <param-name>authsite</param-name> <param-value>localhost|yasuda.homeip.net|margarita|isolde|beatrice</param-value> </init-param> <!-- debug デバッグモード --> <init-param> <param-name>debug</param-name> <param-value>true</param-value> </init-param> </servlet> <!-- BookSearch サーブレットのマッピング --> <servlet-mapping> <servlet-name>BookSearch</servlet-name> <url-pattern>/search</url-pattern> </servlet-mapping> </web-app>
BookSearch Ant ビルド・デプロイ用 build.xml
BookSearch Ant ビルド・デプロイ用 build.xml を以下に掲載する。
ここで,init (初期設定), compile (Java コンパイル), clean (クリーン), deploy (デプロイ), war (war: Web Archive ファイル作成) のレシピを定義している。
<?xml version="1.0" encoding="UTF-8"?> <!-- -*- coding: UTF-8; -*- ブックサーチ on insomnia Ant build 用 XML $Id: build.xml 1 2013-12-20 17:01:27Z isao $ Copyright(c) 2013, isao yasuda, All Rights Reserved. --> <project name="BookSearch" default="compile" basedir="."> <!-- 環境変数 --> <property environment="env" /> <!-- ソースディレクトリ --> <property name="src.dir" value="src" /> <!-- Web アーカイブディレクトリ --> <property name="war.dir" value="war" /> <!-- クラスファイルディレクトリ --> <property name="class.dir" value="${war.dir}/WEB-INF/classes" /> <!-- catalina lib --> <property name="lib.dir" value="${war.dir}/WEB-INF/lib" /> <property name="catalina.lib" value="${env.CATALINA_HOME}/lib" /> <!-- webapp ディレクトリ --> <property name="webapp.dir" value="${env.CATALINA_HOME}/webapps/books" /> <!-- クラスパスの定義 --> <path id="ajax.class.path"> <fileset dir="${lib.dir}"><include name="*.jar" /></fileset> <fileset dir="${catalina.lib}"><include name="*.jar"/></fileset> </path> <!-- クラスファイル出力ディレクトリの作成 --> <target name="init"> <mkdir dir="${class.dir}" /> </target> <!-- Java コンパイル --> <target name="compile" depends="init" description="Compiles all source code."> <javac srcdir="${src.dir}" destdir="${class.dir}" debug="on" encoding="UTF-8" includeAntRuntime="true" classpathref="ajax.class.path" /> </target> <!-- クリーンアップ --> <target name="clean" description="Erases contents of classes dir"> <delete dir="${class.dir}" /> </target> <!-- デプロイ --> <target name="deploy" depends="compile" description="Copies the contents of webapp to destination dir"> <copy file="${src.dir}/log4j.xml" todir="${class.dir}"/> <copy file="bookdb/BOOK.db" todir="${class.dir}"/> <copy todir="${webapp.dir}"> <fileset dir="${war.dir}" /> </copy> </target> <!-- war 作成 --> <target name="war" depends="compile" description="Web Archive"> <delete file="books.war" /> <copy todir="${class.dir}"> <fileset dir="${src.dir}"><include name="log4j.xml" /></fileset> </copy> <copy todir="${class.dir}" file="bookdb/BOOK.db" /> <war destfile="books.war" webxml="${war.dir}/WEB-INF/web.xml"> <lib dir="${lib.dir}" includes="*.jar" /> <classes dir="${class.dir}" /> <fileset dir="${war.dir}"> <exclude name="**/WEB-INF/**" /> </fileset> </war> </target> </project>
Java Servlet アプリケーション開発のリソースを格納するディレクトリ構成は図 1. のとおりである。build.xml のある books 直下ディレクトリにおいて ant war をコマンド発行すると,Java ソースがコンパイルされ,必要な Web アプリケーションリソースを纏めた books.war Web アーカイブファイルが生成される。この war タスクの定義においては,Web アーカイブに収録するに際して,データベースと log4j ロギング設定 log4j.xml を WEB-INF/
図 1. BookSearch Java Servlet 開発ディレクトリ構成
books.war を Tomcat7 manager (http:
図 2. Tomcat7 manager: WAR ファイルの配備
図 3. 配備後アプリケーション一覧
ちょっと長々とし,ごちゃごちゃしてしまった。同じようなプログラムを必要とする方は必ずいると思う。少しでも参考になれば幸いである。有益な参考書もあげておく。