ちょっと 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. 配備後アプリケーション一覧
ちょっと長々とし,ごちゃごちゃしてしまった。同じようなプログラムを必要とする方は必ずいると思う。少しでも参考になれば幸いである。有益な参考書もあげておく。
![Web制作の現場で使うjQueryデザイン入門[改訂新版] (WEB PROFESSIONAL)](http://ecx.images-amazon.com/images/I/51Hq9FIAfIL._SL160_.jpg)


