ラベル Java の投稿を表示しています。 すべての投稿を表示
ラベル Java の投稿を表示しています。 すべての投稿を表示

2009年11月19日木曜日

LDReader 記事詳細の WebView をちょっと修正したり

LDReader 0.0.9 をリリースしました。
・記事詳細の WebView でリンクをクリックしたときにダイアログ表示
・ProgressDialog の表示後の処理を修正

記事詳細の WebView でリンクをクリックしたときにダイアログ表示

記事詳細では間違ってリンクをクリックしてしまってブラウザが起動してしまい、非常にストレスを感じていましたが確認用のダイアログを表示することで少し軽減されるようにしました。ソースはこちら。
// NOTE: WebView#setWebViewClient を使用
bodyView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, final String url) {
// NOTE: 設定でリンクを無効にした場合はそのまま終了
if (ReaderPreferences.isDisableItemLinks(getApplicationContext())) {
return true;
}
// NOTE: ダイアログ表示
new AlertDialog.Builder(ItemActivity.this)
.setTitle(R.string.msg_confirm_browse)
.setMessage(url)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
// NOTE: OK がおされたら Intent を発行
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
return true;
}
});

WebViewClient#shouldOverrideUrlLoading は true を返すことで、WebView の標準を動きをオーバーライドすることができます。

ProgressDialog の表示後の処理を修正

ProgressDialog を表示後に Handler#post を使っていた箇所をすべて Thead を使うようにしました。
実はここ、最初は Thread だったんですが Handler は post の他に postDelayed という遅延実行のメソッドも持っており、Thread のような動きをするので、「あ。こっちでいいのか」とつい思い込みをしてました。

意図しているのは
   プログレスバー表示
↓
重い処理
↓
終了

だったんですが、Handler#post を使うと
   重い処理
↓
プログレスバー表示(一瞬)
↓
終了

になっちゃいます。

ここに詳しい解説がのってました!
http://www.adamrocker.com/blog/261/what-is-the-handler-in-android.html

// NOTE: 修正前
final Handler handler = new Handler();
final ProgressDialog dialog = new ProgressDialog(this);
dialog.show();
handler.post(new Runnable() {
public void run() {
// NOTE: ここで遅い処理
// (some code)
// NOTE: 終わったら ProgressDialog を閉じる
handler.post(new Runnable() {
public void run() {
dialog.dismiss();
}
});
}
});

// NOTE: 修正後
final Handler handler = new Handler();
final ProgressDialog dialog = new ProgressDialog(this);
dialog.show();
new Thread() {
public void run() {
// NOTE: ここで遅い処理
// (some code)
// NOTE: 終わったら ProgressDialog を閉じる
handler.post(new Runnable() {
public void run() {
dialog.dismiss();
}
});
}
}.start();

2009年11月16日月曜日

LDReader フィード一覧の ListView, ListAdapter にハマる

ども。こっそり version 0.0.8 までバージョンアップしました。

[0.0.6] ListView とデータベースとの不整合でたまに例外発生する問題を解消したつもりが失敗。
[0.0.7] android.widget.CursorAdapter を使うことで修正、等
[0.0.8] フィード一覧からピン一覧を開くと、背景が真っ暗になる問題を修正。


[0.0.7] での主な変更

version 0.0.7 以前では、操作していると次のような例外がたまに発生しました。
java.lang.IllegalStateException:
The content of the adapter has changed but ListView did not receive a notification.
Make sure the content of your adapter is not modified from a background thread, but only from the UI thread.

この例外はデータベースのレコード数と ListView のレコード数に違いがあるときに発生します。
LDReader はバックエンドのサービスでレコードを挿入したりしているので自前で色々していたのですが、付け焼刃ではどうしてもこの例外を抑えることができませんでした。
java.lang.Object
↳ android.widget.BaseAdapter
↳ android.widget.CursorAdapter
↳ android.widget.ResourceCursorAdapter

BaseAdapter を継承してゴニョゴニョしていたのですが、探してみると CursorAdapter といクラスがあるではありませんか!

ソースをのぞいて見るとデータベースの更新通知を受け取ってクエリを再発行し、ListView との不整合を解消しているようです。さらに ResourceCursorAdapter では、行ごとの View をリソースを使ってカスタマイズしている場合にピッタリなようです。ApiDemos を見ながら四苦八苦して作ったのですが、まだまだ知らないことがたくさんありそうです。

ちゅうわけで主な変更はこちら。SubscriptionActivity (diff r5)


[0.0.8] での主な変更

Cursor#deactivate を自前でやっていたのですが、Activity#managedQuery を使っていれば deactivate と requery は勝手にやってくれるようです。削除しました。

ちゅうわけで主な変更はこちら。SubscriptionActivity (diff r6)

2009年11月15日日曜日

LDReader ピンに対応しました

関連記事

ども。LDReader をピンに対応させて ver 0.0.5 としました。このブログにコメントいただいちゃったので^ ^
以下スナップショットです。



ちょと見づらいですが、記事タイトル右にピンアイコンがあります。このアイコンをクリックするかメニューから「Pin」を選択するとピンをつけたり外したりできます。

ピンは本家同様に一覧でみることができ、ブラウザで閲覧することができます。
その他、記事 1件の URL をクリップボードにコピーしたり、全件の URL リストをメールで送れるようにしときました。

簡単なバージョンアップですが、今回は少し悩んだ点があります。

単純に実装するとすれば、ピン操作は常にサーバに対して追加や削除を実行し、一覧は最新をサーバから持ってくれば良いです。しかし、これだと地下鉄のようなオフライン環境ではピンを追加することができません。

そこで次のような pin テーブルを作成して、ユーザの操作をまず action として保存し、それから通信を行うようにしました。追加の場合は 1:ACTION_ADD, 削除の場合は 2:ACTION_REMOVE, サーバから取得した場合は 0:ACTION_NONE とします。
create table if not exists pin (
_id integer primary key,
uri text,
title text,
action integer, -- 0:ACTION_NONE, 1:ACTION_ADD, 2:ACTION_REMOVE
created_time integer
)

ピンの追加はロジックはこんな感じ。
public boolean pinAdd(String uri, String title)
throws IOException, ReaderException {
if (!isLogined()) {
login();
}
try {
ContentResolver cr = this.context.getContentResolver();
// NOTE: 事前に uri が重複するピンアクションを削除
cr.delete(Pin.CONTENT_URI, Pin._URI + " = ? and " + Pin._ACTION
+ " > " + Pin.ACTION_NONE, new String[]{uri});
// NOTE: ピンアクションを追加。
ContentValues values = new ContentValues();
values.put(Pin._URI, uri);
values.put(Pin._TITLE, title);
values.put(Pin._ACTION, Pin.ACTION_ADD);
values.put(Pin._CREATED_TIME, (long) (System.currentTimeMillis() / 1000));
Uri pinUri = cr.insert(Pin.CONTENT_URI, values);
// NOTE: 端末がオフラインの場合はここで終了
if (!isConnected()) {
return true;
}
// NOTE: サーバと http 通信。失敗したら IOException などがスローされる。
boolean success = this.client.pinAdd(uri, title);
if (success) {
// NOTE: 通信が成功したら既存の重複ピンを削除して、
// 1:ACTION_ADD を 2:ACTION_NONE に更新。
cr.delete(Pin.CONTENT_URI, Pin._URI + " = ? and " + Pin._ACTION
+ " = " + Pin.ACTION_NONE, new String[]{uri});
values.put(Pin._ACTION, Pin.ACTION_NONE);
cr.update(pinUri, values, null, null);
}
return success;
} catch (ParseException e) {
throw new ReaderException("json parse error", e);
}
}

このままでは、端末がオフラインのときには 1:ACTION_ADD と 2:ACTION_REMOVE が溜まります。
そこで回線がオンラインになったら実行される処理にピンの同期処理を追加します。
public int syncPins() throws IOException, ReaderException {
if (!isLogined()) {
login();
}
ContentResolver cr = this.context.getContentResolver();
// NOTE: 1:ACTION_ADD と 2:ACTION_REMOVE の検索。
String where = Pin._ACTION + " > " + Pin.ACTION_NONE;
String order = Pin._ID + " asc";
Pin.FilterCursor cursor = new Pin.FilterCursor(
cr.query(Pin.CONTENT_URI, null, where, null, null));
try {
while (cursor.moveToNext()) {
// NOTE: 順番にピンアクションを再現
Pin pin = cursor.getPin();
if (pin.getAction() == Pin.ACTION_ADD) {
pinAdd(pin.getUri(), pin.getTitle());
} else {
pinRemove(pin.getUri());
}
Uri uri = ContentUris.withAppendedId(Pin.CONTENT_URI, pin.getId());
cr.delete(uri, null, null);
}
} finally {
cursor.close();
}
// NOTE: サーバからピン一覧を全件取得。
PinsHandler pinsHandler = new PinsHandler();
try {
this.client.handlePinAll(pinsHandler);
} catch (ParseException e) {
throw new ReaderException("json parse error", e);
}
return pinsHandler.counter;
}

関連するソースはこのへん。
@see org.jarx.android.livedoor.reader.Pin
@see org.jarx.android.livedoor.reader.PinActivity
@see org.jarx.android.livedoor.reader.ReaderManager

このピンの実装によってずいぶんと使い勝手が向上したように感じます。ありがとうございます。
次はウィジェットを実装予定です。

2009年1月20日火曜日

Java のフレームワークどうしましょ

あけましておめでとうございます。

次の受託開発案件で Java を使うことになりそうなのでフレームワークの比較検討しています。

私が今まで使ったことのあるフレームワークは
・Struts 1、2系 → EC など
・Tapestry 3系 → 業務システム
・Click Framework → 自社サービス(openidea.jp)
の 3つです。

一言にフレームワークといっても色々な役割がありますが、私は次の 4つが主要な機能だと考えます。
・URI マッピング
・フォームのバリデーション
・テンプレート
・データベースアクセス

表にするとこんな感じでしょうか。かなり昔の記憶で書いているものあって、正確でなかったり、当時スキル不足や調べ切れなかったものもありますが。。


URI マッピングバリデーションテンプレートデータベースアクセス
Strutsサーブレットフィルタ *.do 標準独自JSP 2.0 標準なし
Tapestryサーブレットを /app などにマッピング独自独自 + ognl 形式なし
Click Frameworkサーブレットフィルタ *.htm 変更不可独自Velocity 標準Cayenne、Spring サポート


この 3つはどれもぴったりハマることなく、気に入らないところは色々いじって使ってました。それぞれの使った感想を簡単にあげます。

Struts:
・設定ファイルが面倒。設定ファイルにして便利だったことはほとんどない。
・*Form, *Action とたくさんのクラスができて管理が面倒。
・学習は比較的容易?情報がたくさんある。

Tapestry:
・業務システムで採用したのが間違いだったかもしれないが、複雑な画面になるとイベントの制御がかなり難解になる。
・カスタムコンポーネントを作り始めると面白い。
・ページ制御側の学習コストは非常に高くつく。HTML テンプレートはスッキリ。

Click Framework:
・拡張子 *.htm が固定で変更できないのが痛い。
・学習は容易。

ほとんどのフレームワークはテンプレート部分や、データベースアクセスは他のプロダクトに変更することが可能です。実はここの選定が肝だったりします。別途テンプレートやデータベースアクセス部分のことは書きたいなと思います。

これらの今まで使ってきたフレームワークの追加調査に加え、Wicket も調査したいです。Spring Framework、S2 など DIコンテナを含むものは今回はパスしようと思います。

ここ 2、3年は Perl、PHP 案件がほとんどだったので忘れていましたが、Java 案件は
→ 新しいフレームワークの選定
→ 開発スタートしてから新しいフレームワークの細かい部分の調査ハマる
で遅延しがちなので要注意ですねー。

2008年9月15日月曜日

簡単クラスパス設定 シェルスクリプト

Java のクラスパス、設定するの正直面倒くさいです。そこで少しだけ便利なスクリプトを使っています。使い方はこんな感じ。
Usage: . classpath.sh (option) [dir]
(option)
-a add CLASSPATH
-r recursive

指定したディレクトリを走査して jar ファイルだけを環境変数 CLASSPATH に追加します。以下ソースです。
#!/bin/sh

if [ -z "$1" ]; then
echo "Usage: . classpath.sh (option) [dir]"
echo "(option)"
echo " -a add CLASSPATH"
echo " -r recursive"
exit 0
fi

_recursive=0
_add=0
dir=

parse_arguments() {
for arg do
case "$arg" in
-r) _recursive=1 ;;
-a) _add=1 ;;
*) dir=$arg ;;
esac
done
}

classpath() {
for i in "$1"/*; do
if [ -d "$i" -a $_recursive -eq 1 ]; then
classpath "$i"
elif [ "${i##*.}" = "jar" ]; then
_classpath=$_classpath:$i
fi
done
}

if [ $_add -eq 1 ]; then
_classpath=$CLASSPATH
if [ -z "$_classpath" ] ; then
_classpath=.
fi
fi

parse_arguments $*
classpath "$dir"
export CLASSPATH=$_classpath
unset _classpath

2008年8月7日木曜日

ant-velocity

どもー。

静的コンテンツだけのページを作るとき、どーしてもヘッダフッタをコピペで何度も書きたくないので調べたところ、Apache Jakarta プロジェクトから次の 2つのプロジェクトを見つけました。

・Apache Velocity Anakia
・Apache Velocity Texen

どちらも、Velocity というテンプレートエンジンを使って Ant のカスタムタスクを提供しています。それぞれすばらしいツールなんですが、ちょっと気に入りません。

Apache Velocity Anakia ... XML をソースとして、vsl を記述する必要がある。
Apache Velocity Texen ... コントロールファイルを記述する必要がある。

Ant と Velocity 使うならもっとシンプルにできそうなのに。。そう考えて作っちゃいました!

ant-velocity-1.0.0

もっともシンプルな Ant ビルドファイルはこんな感じ。カスタムタスクを定義して、todir、fileset を指定するだけ。tmpl ディレクトリ以下の html ファイルを Velocity テンプレートとして評価し、htdocs に出力します。
<taskdef name="velocity" classname="org.jarx.ant.VelocityTask"
classpathref="classpath" />
<velocity todir="htdocs" encoding="UTF-8">
<fileset dir="tmpl" includes="**/*.html">
</velocity>

変数を指定したい場合はこう。
<velocity todir="htdocs" encoding="UTF-8">
<fileset dir="tmpl" includes="**/*.html">
<parameter name="baseurl" value="http://jarx.org/" />
</velocity>

変数をプロパティファイルに記述したい場合はこう。
<velocity todir="htdocs" encoding="UTF-8"
propertyFile="resource.properties">
<fileset dir="tmpl" includes="**/*.html">
</velocity>

ボーダーテンプレートを使いたい場合はこう。
<velocity todir="htdocs" encoding="UTF-8"
borderTmplFile="tmpl/border-template.html">
<fileset dir="tmpl" includes="**/*.html">
</velocity>

ボーダーテンプレートはこんな感じ。#parse(${path}) の部分に fileset で指定したテンプレートの内容が入ります。
<html>
<head><title>border template example</title></head>
<body>#parse(${path})</body>
</html>

2008年7月3日木曜日

jar ファイルの場所を見つける方法

自作の jar ファイルに含まれたクラスを使うとき、その jar のパスやディレクトリを知りたいと思ったことありませんか?

かなり昔に作った自作ライブラリの一部ですが、最近、思い出したように使ってみて便利だったので紹介します。
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.ProtectionDomain;
import java.security.CodeSource;

public class ClassUtils {

public static URL findClassLocation(Class c)
throws SecurityException {
Package p = c.getPackage();
String className;
if (p != null) {
className = c.getName().substring(p.getName().length() + 1);
} else {
className = c.getName();
}
URL location = c.getResource(className + ".class");
if (location == null) {
// NOTE: 昔 Tomcat の WEB-INF/lib とかの jar を探すときはこの
// 処理に入ったのだが。。。今は不要かも。
ProtectionDomain domain = c.getProtectionDomain();
CodeSource source = domain.getCodeSource();
if (source != null) {
location = source.getLocation();
}
}
return location;
}

private static File findBaseDirectory(URL classLocation)
throws IOException {
if (classLocation == null) {
return null;
}
String file = classLocation.getFile();
if (classLocation.getProtocol().equals("jar")) {
int i = file.lastIndexOf('!');
if (i != -1) {
file = file.substring(0, i);
}
return findBaseDirectory(new URL(file));
}
File dir = new File(URLDecoder.decode(file,
System.getProperty("file.encoding")));
if (!dir.isDirectory()) {
dir = dir.getParentFile();
}
return dir;
}

public static File findBaseDirectory(Class c)
throws SecurityException, IOException {
return findBaseDirectory(findClassLocation(c));
}

public static void main(String[] args) throws Exception {
System.out.println(findBaseDirectory(java.lang.String.class));
}
}


これがあれば xxx.home などのシステムプロパティ渡さずに済むことが多々あると思います。
sen などもこれに対応してくれればなぁ。。。

2008年6月27日金曜日

Java めんどくさっ

先日 OpenIdea.jp というサービスを初めた記事を書きました。Java で出来ています。でも作ってる途中、感じました。。。

Java めんどくさっ

ちゃんと書くにはいい言語で、総合的には最も好きなんですが、何せ面倒くさい。

そんなとき以下の記事見つけました。
COBOL のように死んだ言語 ~ Java は置き換えられる時期に来ているのか

ふむ。なるほど。。。とりあえず Groovy はじめてみよう。

2008年6月26日木曜日

サーブレットコンテナ(Tomcat)とリバースプロクシを組み合わせた時の問題解決

Tomcat とリバースプロクシを組み合わせた時に微妙な問題に直面しました。その問題の解決方法をメモしておきます。

状況としては、Tomcat は http://localhost:8080/app/ のようにコンテキストパス /app で動作しています。
/app で動作しているアプリケーションに http://app.example.com/ という URL でリバースプロクシの設定を行います。
Tomcat は他のアプリケーションも動作しているので、ルートコンテキストは用いることができません。
http://app.example.com/ (Apache)
↓
(リバースプロクシ)
↓
http://localhost:8080/app/ (Tomcat)

まずはこんな感じで設定しました。問題は 2つあります。
ProxyPath / http://localhost:8080/app
ProxyPassReverse / http://localhost:8080/

1つ目は Cookie 問題です。Tomcat の発行する Set-Cookie の Path 属性が /app となり、ブラウザが / へのアクセスのときにはリクエストヘッダに Cookie をつけて送信しません。
Set-Cookie: JSESSIONID=XXXXXXXXXXXXX; Path=/app

ここでは、ProxyPassReverseCookiePath ディレクティブを用いて、Set-Cookie の Path属性 /app を / に置換します。
ProxyPath / http://localhost:8080/app/
ProxyPassReverse / http://localhost:8080/
ProxyPassReverseCookiePath /app /

これでブラウザへは次のヘッダが返されます。
Set-Cookie; JSESSIONID=XXXXXXXXXXXXX; Path=/

2つ目はコンテキストパス問題です。Tomcat をルートコンテキストで動かしたり、ルートコンテキストは他で使っていても、ポートを変えてもう 1つ Tomcat を起動すれば問題はすぐ解決するのですが、ここは敢えて別の方法をとってみます。

やりたいことは、次の 2つの URL どちらからアクセスしても同じように動くことです。
http://app.example.com/
http://localhost:8080/app/

今回は以下のような方法を使ってみました。

まず、リバースプロクシの設定に加え X-Context-Path という独自のヘッダを付与します。
RequestHeader set X-Context-Path ""
ProxyPath / http://localhost:8080/app/
ProxyPassReverse / http://localhost:8080/
ProxyPassReverseCookiePath /app /

これで Tomcat は、リバースプロクシからきたリクエストを X-Context-Path というヘッダで判断することができます。
次にサーブレットフィルタを作成し、リクエストオブジェクトをラップして、getContextPath メソッドをオーバーライドします。
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
doFilter((HttpServletRequest) req, (HttpServletResponse) res, chain);
}
public void doFilter(HttpServletRequest req, HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
// NOTE: X-Context-Path を受け取って、null で無ければその値を
// コンテキストパスとして使用する。
// ※実際は XSS などを埋め込まれないように文字列チェックする必要
// あります。
final String contextPath = req.getHeader("X-Context-Path");
if (contextPath != null) {
req = new HttpServletRequestWrapper(req) {
public String getContextPath() {
return contextPath;
}
};
}
chain.doFilter(req, res);
}

この他に getServerPort や getRequestURI など変更すべき箇所がたくさんありますが省略。。こうしておけば、http://localhost:8080/app/ にアクセスしたときも http://app.example.com/ にアクセスしたときも期待通りに動作します。

2008年6月25日水曜日

OpenIdea.jp

OpenIdea.jp というサービスを作ってみました。

作った理由:
  • 日ごろ思いつくアイデアを公開したかった。(普段はメールの下書きでためていました。)
  • 何でも良いからサービスを 1つ公開したかった。
苦手な HTML コーディングや、ロゴなども自分でやってます。
Java、ClickFramework、DbUtils、MySQL などで動いてます。まだ検索やランキングも動いてなかったり。。

今のとこ、何ら新しいことはないのですが、実験場として色々追加していく予定です。

今後の予定:
  • アイデアグラフ(最初はツリー表示でいいかな)
  • OpenID 2.0 対応
  • メール投稿
まだプレスリリースとかしてません。もうちょっと触ったらやろうと思います。

2008年4月10日木曜日

意外と面倒くさい autolink

どうも。こんにちは。今回は Wiki などに使われるテキスト中の URL を自動的にリンクする方法について書いてみたいと思います。

Java で正規表現を使って実装するとこんな感じでしょうか。
import java.util.regex.*;
private static Pattern linkPattern = Pattern.compile(
"(https?)(://[\\p{Alnum}\\+\\$\\;\\?\\.%,!#~*/:@&=_-]+)");
public static String link(String s, int length, String suffix) {
StringBuffer buff = new StringBuffer((int) (s.length() * 1.2f));
Matcher m = linkPattern.matcher(s);
while (m.find()) {
String g = m.group();
if (length > 0 && g.length() > length) {
g = g.substring(0, length);
if (suffix != null) {
g += suffix;
}
}
g = m.quoteReplacement(g);
m.appendReplacement(buff, "<a href=\"$1$2\">" + g + "</a>");
}
m.appendTail(buff);
return new String(buff);
}

ところが、ユーザの投稿した文書をこれでフィルタして HTML として出力するときには CSS(クロスサイトスクリプティング)対策としてサニタイジングを行う必要があり、少し注意しなければなりません。例えば、次のような文書をユーザが投稿したとします。
URL サンプルでは <http://example.com/> を使いましょう。

まず、CSS 対策としてサニタイジングします。
URL サンプルでは &lt;http://example.com/&gt; を使いましょう。

これを自動リンクフィルタすると次のようになります。
URL サンプルでは&lt;<a href="http://example.com/&gt;">http://example.com/&gt;</a>を使いましょう。

本来は http://example.com/ をリンクとして出力したいのですが、http://example.com/&gt; がリンクになってしまいます。

これを回避するためにサニタイジングと自動リンクを同時に行います。
private static Pattern linkHtmlPattern = Pattern.compile(
"((https?)(://[\\p{Alnum}\\+\\$\\;\\?\\.%,!#~*/:@&=_-]+))|([&<>\"\'])");
public static String linkHtml(String s, int length, String suffix) {
StringBuffer buff = new StringBuffer((int) (s.length() * 1.2f));
Matcher m = linkHtmlPattern.matcher(s);
while (m.find()) {
String g = m.group();
if (g.length() == 1) {
if (g.equals("&")) {
g = "&amp;";
} else if (g.equals("<")) {
g = "&lt;";
} else if (g.equals(">")) {
g = "&gt;";
} else if (g.equals("\"")) {
g = "&quot;";
} else if (g.equals("\'")) {
g = "&#x27;";
}
m.appendReplacement(buff, g);
continue;
}
if (length > 0 && g.length() > length) {
g = g.substring(0, length);
if (suffix != null) {
g += suffix;
}
}
g = m.quoteReplacement(g);
m.appendReplacement(buff, "<a href=\"$1\">" + g + "</a>");
}
m.appendTail(buff);
return new String(buff);
}
結構面倒くさいです。。色々考えたのですが、今のところこれが一番かなと思います。

Perl の場合は URI::Find という便利なクラスがあります。使い方はこんな感じです。
use URI::Find;
my $finder = URI::Find->new(
sub {
my($uri, $orig_uri) = @_;
return qq {<a href="$uri">$orig_uri</a>};
});
$finder->find(\$text);

では、サニタイジングと併せて使う場合はどうやったらいいのでしょう?すごく悩んでこんな実装にしたことがあります。
$text =~ s/&gt;/ __GT__ /g;
$text =~ s/&lt;/ __LT__ /g;
$text =~ s/&amp;/ __AMP__ /g;
$text =~ s/&quot;/ __QUOT__ /g;
$text =~ s/&#x27;/ __APOS__ /g;
my $finder = URI::Find->new(
sub {
my($uri, $orig_uri) = @_;
return qq {<a href="$uri">$orig_uri</a>};
});
$finder->find(\$text);
$text =~ s/ __GT__ /&gt;/g;
$text =~ s/ __LT__ /&lt;/g;
$text =~ s/ __AMP__ /&amp;/g;
$text =~ s/ __QUOT__ /&quot;/g;
$text =~ s/ __APOS__ /&#x27;/g;

うむむ。もっといい方法ないですかね。。

2008年4月3日木曜日

DbUtils でアンダバー区切りをハンガリアン記法にマッピング

今回は Jakarta Commons の DbUtils について少し書こうと思います。

DbUtils は次のコードサンプルのように SQLクエリの結果を JavaBeans として得ることができるシンプルで便利な機能を持っています。
QueryRunner run = new QueryRunner(dataSource);
ResultSetHandler h = new BeanHandler(Person.class);
Person p = (Person) run.query(
"SELECT * FROM Person WHERE name=?", "John Doe", h);

例えば、Person テーブルの定義が id と name カラムを持つ場合、
create table person (
id integer not null primary key,
name varchar(20) not null
);

Person クラスはこんな感じです。
public class Person implements java.io.Serializable {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

最近 DbUtils を使い始めたのですが、1つ気に入らない問題が出てきました。Java 側の命名規則はハンガリアン記法、データベース側の命名規則はアンダーバー区切りで、次のようなテーブルに対応する JavaBeans は、
create table person (
id integer not null primary key,
first_name varchar(20) not null,
last_name varchar(20) not null
);

first_name カラムは first_name という JavaBeans プロパティにマッピングされるので、専用の setFirst_name などという専用のセッターを用意すればなんとかなりますがあまり美しくありません。
public class Person implements java.io.Serializable {
private int id;
private String firstName;
private String lastName;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName= firstName;
}
public String getLastName() {
return lastName;
}
public void setFirstName(String lastName) {
this.lastName= lastName;
}
// NOTE: for DbUtils
public void setFirst_name(String firstName) {
setFirstName(firstName);
}
public void setLast_name(String lastName) {
setLastName(lastName);
}
}

上記のサンプルが書かれているサイトの下の方にも記述がありますが、
http://commons.apache.org/dbutils/examples.html
org.apache.commons.dbutils.BeanProcessor の mapColumnsToProperties メソッドをカスタマイズすることで first_name から firstName へのマッピングを行うことができます。
import java.beans.PropertyDescriptor;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.Arrays;
import org.apache.commons.dbutils.BeanProcessor;
public class MyBeanProcessor extends BeanProcessor {
@Override
protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,
PropertyDescriptor[] props) throws SQLException {
int cols = rsmd.getColumnCount();
int columnToProperty[] = new int[cols + 1];
Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);
for (int col = 1; col <= cols; col++) {
String columnName = rsmd.getColumnName(col);
for (int i = 0; i < props.length; i++) {
// NOTE: 元のコードを
// if (columnName.equalsIgnoreCase(props[i].getName())) {
// NOTE: 次のように変更
if (equalsColumnProperty(columnName, props[i].getName())) {
columnToProperty[col] = i;
break;
}
}
}
return columnToProperty;
}
private boolean equalsColumnProperty(String colName, String propName) {
// NOTE: 頻繁に呼び出されるので実際にはキャッシュを使うなどして
// 高速化したほうがよいです。
return colName.replaceAll("_", "").equalsIgnoreCase(propName);
}
}

カスタマイズした MyBeanProcessor の使い方はこんな感じです。実際はRowProcessor のインスタンスはシステムに 1つでいいかと思います。
RowProcessor rp = new BasicRowProcessor(new MyBeanProcessor());
QueryRunner run = new QueryRunner(dataSource);
ResultSetHandler h = new BeanHandler(Person.class, rp);
Person p = (Person) run.query(
"SELECT * FROM Person WHERE first_name=?", "John Doe", h);

2008年3月14日金曜日

Java でタグクラウドの実装

Java でタグクラウドの実装をする必要があったので、ググってみましたがなかなかピンとくる情報に出会えなかったため、ネタにしてみます。

幸いにも計算部分は Perl の実装を参考にさせていただくことができたので、Java 風のアレンジを加えてみました。

まずタグのインタフェースを定義します。Comparable を継承しているのはソートに使用するためです。
public interface Tag extends Comparable {
/** タグそのものを返します */
String getValue();
/** タグの使用回数など指標になる数値を返します */
int getScore();
/** タグクラウドの文字の大きさを返します */
int getCloudLevel();
/** タグクラウドの文字の大きさを設定します */
void setCloudLevel(int cloudLevel);
}

次にインタフェース Tag の実装クラスです。
import java.io.Serializable;
public class ExampleTag implements Tag, Serializable {
private String value;
private int score;
private int cloudLevel;
public ExampleTag(String value, int score) {
this.value = value;
this.score = score;
}
public String getValue() {
return value;
}
public int getScore() {
return score;
}
public int getCloudLevel() {
return cloudLevel;
}
public void setCloudLevel(int cloudLevel) {
this.cloudLevel = cloudLevel;
}
public int compareTo(Object o) {
Tag tag = (Tag) o;
return (getScore() - tag.getScore());
}
public String toString() {
return "ExampleTag[value=" + value + ",score=" + score
+ ",cloudLevel=" + cloudLevel + "]";
}
}

最後にタグクラウドの実装です。main にテスト用のコードを書いておきます。TagCloud クラスを作成して Tag オブジェクトを addTag していって最後に getTags で取り出して使います。順序を維持するために java.util.LinkedHashSet を使っています。
import java.util.Iterator;
import java.util.Set;
public class TagCloud {
/** フォントの大きさの段階を指定 */
private final int maxLevel;
/** タグを順序どおりに格納する Set */
private final Set<Tag> tags;
/** 計算済みフラグ */
private boolean clouded;
/** 最小 */
private int minScore = Integer.MAX_VALUE;
/** 最大 */
private int maxScore = Integer.MIN_VALUE;
/** コンストラクタ */
public TagCloud(int maxLevel) {
this(maxLevel, 16);
}
/** コンストラクタ */
public TagCloud(int maxLevel, int capacity) {
if (maxLevel < 1) {
throw new IllegalArgumentException("invalid maxLevel " + maxLevel);
}
this.maxLevel = maxLevel;
this.tags = new java.util.LinkedHashSet<Tag>(capacity);
}
/** タグを追加します */
public void addTag(Tag tag) {
if (tag != null) {
tags.add(tag);
int score = tag.getScore();
this.minScore = Math.min(minScore, score);
this.maxScore = Math.max(maxScore, score);
this.clouded = false;
}
}
/** タグの Set を返します */
public Set getTags() {
cloud();
return tags;
}
/** タグクラウドのための計算をします */
private void cloud() {
final int size = tags.size();
if (clouded || size == 0) {
return;
}
if (size == 1) {
Tag tag = tags.iterator().next();
tag.setCloudLevel(1);
} else {
double min = (minScore == 0) ? 0.0: Math.log(minScore);
double max = (maxScore == 0) ? 0.0: Math.log(maxScore);
double f;
if (min == max) {
min -= maxLevel;
f = 1.0;
} else {
f = maxLevel / (max - min);
}
if (size < maxLevel) {
f *= ((double) size / maxLevel);
}
for (Tag tag: tags) {
int score = tag.getScore();
if (score != 0) {
tag.setCloudLevel((int) ((Math.log(score) - min) * f));
}
}
}
this.clouded = true;
}
// NOTE: for test
public static void main(String[] args) {
TagCloud tagCloud = new TagCloud(4);
tagCloud.addTag(new ExampleTag("abcd", 3));
tagCloud.addTag(new ExampleTag("efg", 1));
tagCloud.addTag(new ExampleTag("hijk", 2));
tagCloud.addTag(new ExampleTag("lmn", 10));
tagCloud.addTag(new ExampleTag("opqrstu", 7));
tagCloud.addTag(new ExampleTag("vwxyz", 5));
System.out.println(tagCloud.getTags());
}
}

上記 3つのソースを同じディレクトリにおいて javac *.java でコンパイル、java TagCloud でテスト実行すると以下のような結果が出力されます。
[ExampleTag[value=abcd,score=3,cloudLevel=1],
ExampleTag[value=efg,score=1,cloudLevel=0],
ExampleTag[value=hijk,score=2,cloudLevel=1],
ExampleTag[value=lmn,score=10,cloudLevel=3],
ExampleTag[value=opqrstu,score=7,cloudLevel=3],
ExampleTag[value=vwxyz,score=5,cloudLevel=2]]

このクラスを利用して JSP などで HTML 出力すれば
<style type="text/css">
.tagcloud {width:180px;}
.tagcloud span {padding:5px; font-weight:bold; color:steelblue;}
.tagcloud .tag0 {font-size:10px;}
.tagcloud .tag1 {font-size:16px;}
.tagcloud .tag2 {font-size:22px;}
.tagcloud .tag3 {font-size:28px;}
</style>
<div class="tagcloud">
<span class="tag1">abcd</span>
<span class="tag0">efg</span>
<span class="tag1">hijk</span>
<span class="tag3">lmn</span>
<span class="tag3">opqrstu</span>
<span class="tag2">vwxyz</span>
</div>


こんな感じのタグクラウドになります。