インクリメンタルサーチ

技術評論社のWEB+DB PRESS Vol.27Ajaxの特集を発見。なかでも「第2章 実践! Ajaxアプリケーション開発」はJSONを使っているので、真面目に読んでみた。なるほど。XMLHttpRequestプロトコルで、それに乗せてサーバからクライアント(Webブラウザ)へJSONオブジェクトを送る、というわけか。他の選択肢についても良く整理されていてわかりやすい。なんか断片的だった情報がつながった感じがした。

じゃ実際にインクリメンタルサーチをやってみますか。XMLHttpRequestはブラウザに実装されているので(これもこの記事を読むまで知らんかった(汗)、JSONを手に入れる。といってもJSON自体はデータ構造の定義。詳しくは

http://www.crockford.com/JSON/index.html

を見るべし。実際に必要なのはJavaでこのJSONをハンドリングするライブラリ。今回は

http://www.crockford.com/JSON/java/index.html

で公開されている、以下のJavaクラスを利用した。

jarファイルでもあるかと思ったらなかった。それぞれソースをCOPYして、Eclipseコンパイル。そしてWEB+DB PRESS Vol.27 補足情報から掲載記事のソースをプログラムを入手。Webブラウザ側のwebdbgrep.htmlはそのままでいいはずだから、サーバ側でリクエスト(インクリメンタルサーチ)を処理するwebdbgrep.phpに代わるものをJavaで作ればいいわけだ。で、作ってみたのがこれ。

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONArray;

public class MyWebdbgrep extends HttpServlet {
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		String dirname = "...."; // 検索対象のdir

		response.setContentType("text/xml; charset=UTF-8");
		PrintWriter out = response.getWriter();

		// keyword取得
		String keyword = null;
		String[] tmp = request.getParameterValues("keyword");
		try {
			byte[] tmpbyte = tmp[0].getBytes("ISO_8859_1");

			keyword = new String(tmpbyte, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			System.out.println(e);
		}
		

		// 当該dirのfile/dir一覧を取得
		File dir = new File(dirname);
		File[] files = dir.listFiles();

		BufferedReader br;
		String filename;
		String line;
		int linec;
		int hit = 0;

		// 検索結果を入れるJSONオブジェクト
		JSONArray result = new JSONArray();
		for (int i = 0; i < files.length; i++) {
			// ファイルだけ対象
			if (files[i].isFile()) {
				filename = files[i].getName();
				br = new BufferedReader(new FileReader(files[i].getAbsolutePath()));
				linec = 0;
				while ((line = br.readLine()) != null) {
					++linec;
					if (line.indexOf(keyword) > 0) {
						++hit;
						// 結果1件分を表すJSONオブジェクト
						JSONArray array = new JSONArray();
						array.put(filename);
						array.put(linec);
						array.put(line);
						result.put(array);
					}
				}
				br.close();
			}
		}
		// ブラウザへ返す
		out.println(result.toString());
	}
}

これをwebdbgrep.htmlから呼び出してみると...。ダメだ。うごかねー。ん?FireFoxでは動く?でも変だ。整理してみると、

  • IE6.0では何も表示されない。
  • FireFox1.0では1つ前の結果が表示される。

という感じ。MyWebdbgrepは動いているっぽいから、こうなるとwebdbgrep.htmlだろうな。いきなりJavaScriptデバッグかよw。webdbgrep.htmlをよーく見てたら、onloadで呼ばれるstartup()の中が

function startup() {
	document.form1.text1.onkeyup = function() {  //(a)
		xmlhttp.onreadystatechange = function() { //(b)
						:
			//検索結果をセットする処理(c)
						:
		}
		//XMLHttpRequestで検索を実行(d)
	}
}

という構造になっているのが引っ掛る。これだと最初の文字が入力されて(a)に来たときに、(b)をコールバック関数として設定する。このときはそのまま(d)に行き、リクエストを投げて終わり。このタイミングで(b)は実行されないようだ。次の文字が入力されて(a)に来て、再び(b)がセットされると、前回の(1文字目入力で実行された)結果が返ってきているので(c)が実行される?試しに

function startup() {	//(a)
	document.form1.text1.onkeyup = function() {//XMLHttpRequestで検索を実行(d)
		xmlhttp.abort();
		xmlhttp.onreadystatechange = handleHttpEvent;	//(b)
		xmlhttp.open("GET", "http://.../mywebdbgrep?keyword=" + encodeURI(this.value), true);
		xmlhttp.send(null);
				:
	}
}

function handleHttpEvent(){
	//検索結果をセットする処理(c)
}

といった感じでXMLHttpRequestをハンドルする関数を独立させてみたらうまく行った。IEでも同じように動く。微妙なタイミングの違いか、それともMyWebdbgrepの作りの問題か。まぁいいや。ともかく、おもしろい。

あと良く見ると、IMEで入力したときonkeyupの発生タイミングがIEFireFoxでは違うみたいね。

  • FireFox:確定したとき1回発生
  • IE:物理的にキー入力すると発生する

もっとも日本語もインクリメンタルサーチの対象にするなら、

String keyword = null;
String[] tmp = request.getParameterValues("keyword");
try {
	byte[] tmpbyte = tmp[0].getBytes("ISO_8859_1");

	keyword = new String(tmpbyte, "UTF-8");
} catch (UnsupportedEncodingException e) {
	System.out.println(e);
}

見たいな感じで、リクエストパラメータを取り出す必要がある。POSTで渡せばsetCharacterEncoding()が使えるからこういうコードは不要。でもそのときは、

xmlhttp.setRequestHeader("content-type", "application/x-www-form-urlencoded;charset=UTF-8");

でヘッダーを上書きする必要があるみたい。これ入れたらうまく行きました。

なるほどAjaxってこういう感じなんだ。結構使えるかもしれない。