ちょっと 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | // -*- 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 要素を操作していたのが懐かしくなる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 | // -*- 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({ 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 書籍データベースのパス,後者はアプリケーション要求を受け付けるドメインの正規表現指定になっている。後者は特定サイト以外のページからの利用ができないようにするための工夫である(完全ではない)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" <!-- -*- 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 ファイル作成) のレシピを定義している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | <? 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. 配備後アプリケーション一覧
ちょっと長々とし,ごちゃごちゃしてしまった。同じようなプログラムを必要とする方は必ずいると思う。少しでも参考になれば幸いである。有益な参考書もあげておく。