Home日記コラム書評HintsLinks自己紹介
 

Hints: Java

Hints: Java

Java に関するどうでもいいことから訳の分からないことまで、 メモ的に書いてみることにする。

Jakarta Commons の HttpClient で日本語のフォームを渡す (2)

Jakarta Commons の話。 post するときの文字コードの指定方法。 HTTP Request header で characterEncoding を指定するには、 次のような手順が必要らしい。

    PostMethod method = new PostMethod(uri);
    method.getParams().setContentCharset(getContentCharset());

    // これがないと characterEncoding がセットされない
    method.setRequestHeader("Content-Type", "text/html; charset="
            + getContentCharset());

とりあえずうまく動作しているのだが、 これが正攻法なのか、 ちょっと自信がない。

(2006-05-25)

JNDI で DNS を引く

Bulkfeeds が、 スパム URL のリストを公開している。 プログラムから参照するためのサービスも提供しているのだが、 Web Service とかではなく、 DNS で引くという瓢箪から駒みたいなインターフェースになっている。

このサービスを使うには、 DNS に対して、 [domain].rbl.bulkfeeds.jp の A レコードを引く。 もしスパムとして登録されていたら、 127.0.0.2 というアドレスを返すのだが、 登録されていなければ not found というか、見つからないわけだ。 なるほどー。

例えば、phinloda.com.rbl.bulkfeeds.jp というのは登録されていないから、 これはセーフ、ということが分かるのだ。 詳細は、 http://bulkfeeds.net/app/blacklist をご覧ください。

では Java からどうすればチェックできるか?

import java.util.Properties;

import javax.naming.Context;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.InitialDirContext;

    public boolean isBlack(String domain) {
        boolean result = false;

        String name = domain + bulkFeeds;
        Properties properties = new Properties();
        properties.put(Context.INITIAL_CONTEXT_FACTORY,
                "com.sun.jndi.dns.DnsContextFactory");
        properties.put(Context.PROVIDER_URL, getDnsUri());

        try {
            InitialDirContext dirContext = new InitialDirContext(properties);
            Attributes attrs = dirContext.getAttributes(name);
            Attribute attribute = attrs.get("A");
            String value = (String) attribute.get();
            result = value.equals("127.0.0.2");

        } catch (Exception e) {
            // ここに来たら spam ではないということ
        }

        return result;
    }
    
    private final String bulkFeeds = ".rbl.bulkfeeds.jp";

    private String dnsUri; // "dns://192.168.0.1"

    public String getDnsUri() {
        return dnsUri;
    }

    public void setDnsUri(String dnsUri) {
        this.dnsUri = dnsUri;
    }

こんな感じか。 domain の null check とかしてないから、 本当に使うときには気をつけてほしい。 isBlack を呼ぶ前に、setDnsUri で、 有効な DNS の URI をIPで指定する。 例えば dns://192.168.0.1 のような感じ。

(2006-04-20)

Jakarta commons lang, DateUtils

日付の処理をするときに便利。

static Calendar round(Calendar calendar, int field);

日付を指定したフィールドで丸める。 即ち、指定した精度で近い方の日付に修正する。 例えば、 field として Calendar.YEAR を指定すると、 2006-03-13 から 2006年の最初の日、 つまり 2006-01-01 を得ることができる。 2005-12-01 なら、近い方ということで、2006-01-01 が得られる。

同様に、 Calendar.MONTH を指定すれば、 どちらか近い月の1日の日付を得ることができる。

static Calendar truncate(Calendar calendar, int field);

日付を指定したフィールドで切り捨てる。 Calendar.YEAR を指定すると、 2005-12-31 から 2005-01-01 を生成する。

例: 今日が含まれる四半期の開始日を求める。 ただし、1月1日に期が始まるものとする。

    newCalendar = DateUtils.truncate(calendar, Calendar.MONTH);
    newCalendar.set(Calendar.MONTH, newCalendar.get(Calendar.MONTH)
                    - (newCalendar.get(Calendar.MONTH) % 3));

(2006-03-13)

Eclipse に CVS repositry を追加したときのメモ

多分、見ても分からないと思うが、 自分的に役に立つので、 もしかしてということもあるので。

状況としては、 CVS に登録されている Tomcat Project を新規取得し、 ローカルの環境で実行できるようにするのが目標。 Tomcat project をデフォルトで新規作成したときとは、 WEB-INF の位置が異なっている。 外部 jar は、WEB-INF/lib に全て登録されている。

File - New - Other でダイアログを表示して、 CVS - Checkout Projects from CVS を選択して、 Next を押す。

Checkout Project from CVS Repository のダイアログが表示される。 Use existing repository location を選択して、 表示されている内容を選択し、 Next を押す。

Select Module の画面で、 Use an existing module (this will allow you to browse the modules in the repository) を選択し、 目的のターゲットを選択して、 Finish ではなく、Next を押す

Check Out As の画面で、 Check out as a project configured using the New Project Wizard を選択して、 Next を押す。

Select Tag の画面で、 Head を選択して、 Finish を押す。

New Project のダイアログが表示されるので、 Java - Tomcat プロジェクトを選択して、 Next を押す。

Project name: に適当な名前を入れて (例えば hoge_1)、 next を押す。

Tomcat 用プロジェクトの設定で、 アプリケーション URI を /hoge_1 ではなく /hoge_1_servlet などとし、 Finish を押す。

作成されたプロジェクトを選択し、右クリックメニューで Properties を実行する。 Java Build path を実行する。

Soure タグをクリックし、 Add Folder ボタンを押す。

CVS に登録されているデータ上にソースファイルがある位置を確認し、 チェックして OK を押す。

Tomcat project のデフォルトで作成されている、 */WEB-INF/src というフォルダを選択して、Remove を押す。

Default output folder の Browse ボタンを押し、 CVS に登録されている構造の WEB-INF を選択する。 を選択する。

Create New Folder ボタンを押し、 表示されたダイアログで、 Folder name を classes と指定する。

この classes を選択し、OKを押す。

Libraries タブを選択する。

Add External JARs ボタンを押す。

.../WEB-INF/lib にある .jar ファイルをすべて追加する。

test の適当なソースを開き、 ソース中の x が表示されている行で quick fix を実行し、 Add JUnit libraries を指定する。

properties を閉じると、自動的に再コンパイルが始まる。

(2006-03-13)

Eclipse のソース

anonymous CVS で公開されている。 www.eclipse.org の Downloads のタブをクリックし、 Source code をクリック。

Eclipse を起動して CVS repositry ビューを使うのが簡単。

Host: dev.eclipse.org
Repository path: /cvsroot/eclipse
User: ananymous
Password:
Connection type: pserver

パスワードは空欄のままでよい。

(2006-03-08)

FastDateFormat (Jakarta commons lang)

    private FastDateFormat dateFormat = 
        FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss.S");
    
    public String getDateString(Date date) {
        return this.dateFormat.format(date);
    }

(2006-03-07)

Sen と OutOfMemoryException

といいつつ、 Sen の話ではありません。すみません。

Sen を使って大きなファイルや、そこから作成した長大な文字列を食わせると、 OutOfMemory になってしまうことがある。 heap size を大きくするというのも対策になるかもしれないが、 本質的に、 Sen は形態素解析するだけの処理なのだから、 明らかに分割できる所で区切って処理をさせて、 結果を集計した方が合理的だろう。

ファイルを1つのString にしたなら、 空白行で区切るというのがよい。 Java の標準 API だけで書ける。

    public String[] splitText(String text) {
        // 頻繁に呼ぶようなら 外に出すとよい
        Pattern pattern = Pattern.compile("(?m)^$");

        return pattern.split(text);
    }

プレーンテキストならこれでもいいが、 HTML や XML だと、 タグで区切ってやれば本文っぽい所だけ解析することができていいかも。 タグを除去することで、 タグの中の文字列が頻出することへの対策にもなる。 ただし、タグの中の情報も解析対象にしたければ、 もう少し考える必要がある。

さっきの処理と同様のものを作って、 とりあえず、 正規表現としては、こんなのを指定すればいいと思う。

"(?m)<.*?>(\\s*<.*?>)*"    

(2006-02-22)

Spring Framework の SessionFactoryUtils にある getSession と doGetSession の違い

org.springframework.orm.hibernate.SessionFactoryUtils のソースを見ると、こうなっている。

	public static Session getSession(SessionFactory sessionFactory, boolean allowCreate)
	    throws DataAccessResourceFailureException, IllegalStateException {

		try {
			return doGetSession(sessionFactory, null, null, allowCreate);
		}
		catch (HibernateException ex) {
			throw new DataAccessResourceFailureException("Could not open Hibernate Session", ex);
		}
	}

つまり、getSession はその中で doGetSession を呼び出して、 HibernateException が発生したときに DataAccessResourceFailureException を投げる、 ということですか。

tomcat 5.5.15 が動かない?

ちなみに、5.5.11 から 5.5.15 にしたら、 JRE が何だかという話で全然動かなくなった。 5.5.15 は Java 5.0 でないと動かないので、そういう話のようだ。 もちろん、CLASSPATH とか PATH には指定したつもりなのだが、 どうもよく分からない。

こういうときは、 インストールした所に logs ディレクトリがあるから、 その中の最新の情報をチェックする、というのが基本。 しかしよく分からない。

とりあえず、J2SE 5.0 を uninstall して、 デフォルトの場所 (C:\Program Files\Java\…) にインストールしてみたら、 動くようになった。謎増。 後で調べたところ、 どうも JRE を自動的に見つけることができなかったようで、 JRE がインストールされているディレクトリを JRE_HOME が指すように設定したらいいようだ。

移行手順。 server.xml を修正する。 元の設定をコピーするという大胆なことをする勇気はないので、 変更箇所をとりあえずチェック。 このような時のために、 server.xml.orig というファイルを作っておいて、 デフォルトとの差分がいつでも分かるようにしておくと便利。

とりあえず、SSL を使えるようにした箇所と、 Context をいくつか追加したところが問題。 まず、SSL は、

Define a SSL HTTP/1.1 Connector on port 8443

となっている設定がコメントアウトされているので、 コメントの記述を取って実行できるようにする。

Context の方は、 ファイルの最後の方にいくつか追加されているので、 これをコピーしておく。

web.xml。 default servlet の listings のところが false になっているので true にする。 今回使う tomcat は、 内部テスト用で、外からはアクセスしないので、 特にそのあたりのセキュリティは意識しないでいい。 公開するものであれば、false にしておいた方がいい。

(2006-01-27)

ant で新しいファイルだけ上書き

よく使う機能なのだが、 細かいところをうっかりしがちでもある。

    <property name="src.dir" value="C:/home/mai/work/java/practice" />
    <property name="dest.dir" value="C:/eclipse/workspace/practice/src" />

    <target name="copyUpdated">

        <copy todir="${dest.dir}" preservelastmodified="true" verbose="true">
            <fileset dir="${src.dir}">
                <include name="**/*.java" />
                <different targetdir="${dst.dir}" ignoreFileTimes="true" />
            </fileset>
        </copy>
    </target>

${dest.dir} から ${src.dir} にディレクトリ構造を壊さずにコピーする。 いずれも、property で設定しておくとして、 これだとコピーしたときにファイルの更新時刻が変更されてしまって、 不都合が発生することがある。 というか、あった。 preservelastmodified のデフォルトは false なので、 ファイルの時刻を保ったままコピーしたいのなら、 これを true にしておかなければならない。

(2006-01-24)

log4j のデバッグ出力を選択する

デバッグ時に、log4j.properties の出力を debug と指定すると、 大量のログができてしまって訳が分からなくなることがある。 例えば、tomcat のログ出力で、 Digester の debug 出力は必要ない場合には、 次のような設定を追加する。

log4j.logger.org.apache.tomcat.util.digester=INFO, R
log4j.logger.org.apache.commons.digester.Digester=INFO, R

(2006-01-17)

toString

クラス内のメンバ変数を見やすく表示するために toString を override するのは面倒なのだが、 commons.lang に便利なクラスがある。

import org.apache.commons.lang.builder.ReflectionToStringBuilder;

 ~略~

    public String toString() {
        return ReflectionToStringBuilder.toString(this);
    }

メンバ変数に List 等がある場合は、 複数行出力を選択すると見やすくなることがある。

    public String toString() {
        return ReflectionToStringBuilder.toString(this, ToStringStyle.MULTI_LINE_STYLE);
    }

(2005-12-22)

Webページをファイルに保存する

Jakarta Commons IO を使う。

    FileUtils.copyURLToFile(Url url, File file);

ファイルに保存できればいいのであれば、 かなり楽。 これに対して、HttpClient を使った場合は、 多少面倒ではあるが、 クッキーの処理とか、 POST の処理なども行えるというメリットがある。

(2005-12-20)

Apache axis

install:

WebServices - Axis が本家サイト。 現時点 (2005-12-15) の最新版 (1.3) は、 Apache Download Mirrors からダウンロードできる。

Tomcat と連携させることにする。 Axis installation instructions の、Step 2: Setting up the libraries に、 Tomcat 4.x and Java 1.4 という項目がある。

Java 1.4 changed the rules as to to how packages beginning in java.* and javax.* get loaded. Specifically, they only get loaded from endorsed directories. jaxrpc.jar and saaj.jar contain javax packages, so they may not get picked up. If happyaxis.jsp (see below) cannot find the relevant packages, copy them from axis/WEB-INF/lib to CATALINA_HOME/common/lib and restart Tomcat.

ということで、そのようにする。

CLASSPATH は、次の jar が入るように設定しなければならない。

axis.jar
commons-discovery.jar
commons-logging.jar
jaxrpc.jar
saaj.jar
log4j-1.2.8.jar
the XML parser jar file or files (e.g., xerces.jar)

これらは、実際は axis-1_3/lib に次のファイルが入っている。

axis.jar
commons-discovery-0.2.jar
commons-logging-1.0.4.jar
jaxrpc.jar
saaj.jar
log4j-1.2.8.jar

これ以外に、 lib には次のファイルも入っている。

axis-ant.jar
axis-schema.jar
wsdl4j-1.5.1.jar

これらを AXISCLASSPATH にセットするのに、 バッチファイルを作成し、 次のような記述を含める。

set AXIS_HOME=d:\java\axis-1_3
set AXIS_LIB=%AXIS_HOME%\lib
set AXISCLASSPATH=%AXIS_LIB%\axis.jar;
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\commons-discovery-0.2.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\commons-logging-1.0.4.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\jaxrpc.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\saaj.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\log4j-1.2.8.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\axis-ant.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\axis-schema.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\wsdl4j-1.5.1.jar

jar を一つずつ分けて書いているのは、 JAVA の CLASSPATH を設定するときに、 他のアプリケーションとの都合で、 別の場所に配置されている同じ jar ファイルを指定することがあるのだが、 それを二重に読むのを避けるときに、 このように書いておくと便利なのだ。 例えば、 CLASSPATH に既に commons-logging-1.0.4.jar が含まれているとする。

set AXIS_HOME=d:\java\axis-1_3
set AXIS_LIB=%AXIS_HOME%\lib
set AXISCLASSPATH=%AXIS_LIB%\axis.jar;
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\commons-discovery-0.2.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\jaxrpc.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\saaj.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\log4j-1.2.8.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\axis-ant.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\axis-schema.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\wsdl4j-1.5.1.jar

set %CLASSPATH%=%CLASSPATH%;%AXISCLASSPATH%

set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\commons-logging-1.0.4.jar

こういう書き方ができる。 ただし、こうすると、 CLASSPATH の指している commons-logging-1.0.4.jar と、 AXISCLASSPATH が指しているものとが、 別の実体になってしまう。 これを避けるには、 次のような面倒なことをする必要がある。

set LOGGINGCLASSPATH=d:\java\commons-logging-1.0.4\commons-logging.jar

set AXIS_HOME=d:\java\axis-1_3
set AXIS_LIB=%AXIS_HOME%\lib
set AXISCLASSPATH=%AXIS_LIB%\axis.jar;
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\commons-discovery-0.2.jar
rem set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\commons-logging-1.0.4.jar

    ~途中略~

set CLASSPATH=%CLASSPATH%;%LOGGINGCLASSPATH%
set CLASSPATH=%CLASSPATH%;%AXISCLASSPATH%

set AXISCLASSPATH=%AXISCLASSPATH%;%LOGGINGCLASSPATH%

Java2WSDL

Axis User's Guide を見よ。 とりあえず、 先に書いたような環境変数の設定ができていれば、 Java2WSDL が動くはず。 こちらの環境の setpath.bat (Windows XP Professional) は、こんな状態。

set JAVA_HOME=D:\j2sdk1.4.2_10
set FORREST_HOME=D:\java\apache-forrest-0.7
set ANT_HOME=d:\java\apache-ant-1.6.5
set STRUTS_HOME=d:\java\struts-1.2.7
set CATALINA_HOME=d:\tomcat\jakarta-tomcat-5.5.9

set LOGGINGCLASSPATH=d:\java\commons-logging-1.0.4\commons-logging.jar

set AXIS_HOME=d:\java\axis-1_3
set AXIS_LIB=%AXIS_HOME%\lib
set AXISCLASSPATH=%AXIS_LIB%\axis.jar;
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\commons-discovery-0.2.jar
rem set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\commons-logging-1.0.4.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\jaxrpc.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\saaj.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\log4j-1.2.8.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\axis-ant.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\axis-schema.jar
set AXISCLASSPATH=%AXISCLASSPATH%;%AXIS_LIB%\wsdl4j-1.5.1.jar
set CLASSPATH=.
set CLASSPATH=%CLASSPATH%;d:\tmp\w2k\junit3.8.1\junit.jar
set CLASSPATH=%CLASSPATH%;d:\java\eclipse\plugins\org.eclipse.swt.win32_2.1.0\ws\win32\swt.jar
set CLASSPATH=%CLASSPATH%;d:\java\javamail-1.3.2\mail.jar
set CLASSPATH=%CLASSPATH%;d:\java\jaf-1.0.2\activation.jar
set CLASSPATH=%CLASSPATH%;d:\java\commons-httpclient-3.0-rc2\commons-httpclient-3.0-rc2.jar
set CLASSPATH=%CLASSPATH%;%LOGGINGCLASSPATH%
set CLASSPATH=%CLASSPATH%;d:\java\commons-codec-1.3\commons-codec-1.3.jar
set CLASSPATH=%CLASSPATH%;%AXISCLASSPATH%

set AXISCLASSPATH=%AXISCLASSPATH%;%LOGGINGCLASSPATH%

set Path=D:\cygwin\bin;C:\WINDOWS\system32;C:\WINDOWS;%JAVA_HOME%\bin;%ANT_HOME%\bin

D:\java\axis_work という作業用ディレクトリにこれを入れて、 setpath.bat を実行してから、 次のコマンドを実行する。

java org.apache.axis.wsdl.Java2WSDL

すると、次のようにエラーが表示される。

<class-of-portType>が指定されていませんでした / [en]-(The <class-of-portType> was not specified.)
Java2WSDL 生成器 / [en]-(emitter)
使用法: java org.apache.axis.wsdl.Java2WSDL [options] class-of-portType / [en]-(
Usage: java org.apache.axis.wsdl.Java2WSDL [options] class-of-portType)
オプション: / [en]-(Options:)
        -h, --help
                このメッセージを出力し終了します / [en]-(print this message andexit)
        -I, --input <argument>
                入力WSDLファイル名 / [en]-(input WSDL filename)
        -o, --output <argument>
                WSDLファイル名の出力 / [en]-(output WSDL filename)
 ~略~

要するにこれはヘルプのメッセージだ。 これは動いているのかな? サンプルを参考にして、 手元にある適当なクラスを指定し、 WSDL が生成されることを確認してみよう。 今回は、 com.phinloda.practice.hibernate.SiteInfo を実験してみた。 axis_work には、 com/phinloda/practice/hibernate/SiteInfo.class をディレクトリ階層ごとコピーしておく。

Note

この直前に、別の環境で axis を動かしたら、 Exception が出てどうにもならなかったのである。 それを検証しようとして、 VAIO Z の Windows XP Pro に同じ環境を作ったら、 何の問題もなく動いてしまったわけだ。
java org.apache.axis.wsdl.Java2WSDL -o wp.wsdl  
    -l"http://localhost:8080/axis/services/SiteInfoService" -n "urn:SiteInfo" 
    -p"com.phinloda.practice.hibernate" "urn:SiteInfo" com.phinloda.practice.hibernate.SiteInfo

実際は1行で入力している。 これを実行すると、 siteinfo.wsdl ができている。

(この項、執筆中です)

MySQL 4.1.x に update したら文字化け

要するに全部 utf8 にすればいいという話だが、 Hibernate を使っていたりするので、かなり奥が深い。 mysqld を次のようなオプションで起動する。

mysqld --set-variable=character_set_server=utf8 --set-variable=character_set_client=utf8 --character-set-server=utf8

mysql を使って次のコマンドを確認してみる。

show VARIABLES LIKE 'character\_set\_%';

+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| character_set_client     | utf8   |
| character_set_connection | utf8   |
| character_set_database   | latin1 |
| character_set_results    | utf8   |
| character_set_server     | utf8   |
| character_set_system     | utf8   |
+--------------------------+--------+

character_set_database が latin1 になっている。 これが utf8 にできれば問題は解決しそうだが、 utf8 にする方法が分からない。

そこで、create table するときに、

create table tablename ( ... ) DEFAULT CHARACTER SET = utf8;

のように、character set を指定してやるとよい。 ところが、 table は hibernate tools (SchemaExportTask) を使っているために、 このように create table に DEFAULT CHARACTER SET を追加する方法が分からない。

そこで、とにかく SchemaExportTask を一度実行し、 そのときに表示されるテーブル生成のコマンドをパクって修正することにした。 例えば、こんな出力があったとして、

create table kvset (
kvset_id bigint not null,
primary key (kvset_id)
)

次のように変更する。

create table kvset (
kvset_id bigint not null,
primary key (kvset_id)
) DEFAULT CHARACTER SET = utf8;

ポイントは、各 SQL の最後にセミコロン「;」を追加することと、 create table の最後に DEFAULT CHARACTER SET = utf8; を追加すること。 手作業でもできる分量だが、 次のような perl スクリプト、 createsql.pl を用意した。

#! /usr/local/bin/perl

$filename = "schemaexport.txt";
open IN, $filename or die "can't read $filename\n";

my $in_create = 0;
my $database = "practice";

print "use $database\n";

while (<IN>) {
  next unless /^\[schemaexport\]\s+(.+?)$/;
  $tmp = $1;

  next if $tmp =~ /^log4j:/;
  if ($tmp =~ /^create/) {
    $in_create = 1;
  }

  if ($in_create == 1 && $tmp =~ /^\)/) {
    $tmp .= " DEFAULT CHARACTER SET = utf8";
    $in_create = 0;
  }

  print $tmp;
  print ";" if $in_create == 0;
  print "\n";
}

close IN;

exit 0;

なんだこれは、とか言われそうだが、 3分で書いたのでこんなものだ、 というか、 必ず use strict とか付けて使うこと。

Eclipse のコンソールに出力された出力を丸ごとコピーして schemaexport.txt というファイルに保存する。

perl createsql.pl > createtable.sql

このようにして作った SQL に対して、 次のコマンドを実行する。

mysql -u username -p < createtable.sql

これで現在のテーブルを DEFAULT CHARACTER SET を指定したテーブルに置き換えることができる。 このときに、あらかじめテーブルがないと失敗することに注意。 ant schema を実行しておいてから、 その出力ログをファイルに保存し、 ただちに一連の手順を実行するのがよい。

参考にしたサイト

MySQL4.1 - Ground-SunLight:
mysql:9687
MySQL 4.1.xで文字化け - cles::blog (このページに書かれている通りにすれば簡単に解決するのかもしれないが、 今回は試していない)
MySQL 4.1 リファレンスマニュアル :: 4.6.8.4 SHOW VARIABLES

(2005-12-08)

Eclipse で JUnit 実行時に ConnectException

JUnit を実行したら、 こんなエラーが出た。

Could not connect to:  : 4326
java.net.ConnectException: Connection refused: connect
    at java.net.PlainSocketImpl.socketConnect(Native Method)
    at java.net.PlainSocketImpl.doConnect(PlainSocketImpl.java:305)
    at java.net.PlainSocketImpl.connectToAddress(PlainSocketImpl.java:171)
    at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:158)
    at java.net.Socket.connect(Socket.java:452)
    at java.net.Socket.connect(Socket.java:402)
    at java.net.Socket.<init>(Socket.java:309)
    at java.net.Socket.<init>(Socket.java:124)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.connect(
        RemoteTestRunner.java:754)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(
        RemoteTestRunner.java:336)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(
        RemoteTestRunner.java:196)

Eclipse を一度終了させて、再起動したら、 このエラーは二度と発生しなくなった。

(2005-11-27)

Forrest の Document-2.0 に通らないドキュメントの対応方法

Java ではないのだが、とりあえずここに。 アフェリエイト用のバナーなどを指定通りに処理したい場合、 そのままでは validator で拒絶されたり、 xslt を通らないことがあるので、 forrest の定義ファイルを修正して強引に変更する。

img に属性を追加する

$FORREST_HOME/main/webapp/resources/schema/dtd/document-v20.mod 中の img の定義を変更する。 例えば、 border 属性を追加する場合、 次の太字の行を追加する。

<!ATTLIST img
  src CDATA #REQUIRED
  alt CDATA #REQUIRED
  height CDATA #IMPLIED
  width CDATA #IMPLIED
  usemap CDATA #IMPLIED
  ismap (ismap) #IMPLIED
  border CDATA #IMPLIED
  %common.att; 
>

iframe タグを追加する

amazon のリンクに対応するために、 この変更を行った。 $FORREST_HOME/main/webapp/resources/schema/dtd/document-v20.mod の変更箇所は2つ。 まず、次の行。

<!ENTITY % special-inline "br|img|icon|acronym|map">

これを次のように変更する。

<!ENTITY % special-inline "br|img|icon|acronym|map|iframe">

さらに、適当なところ(例えば、img の後など)に、次の定義を追加する。

<!-- Iframe (added by Mai) -->
<!ELEMENT iframe (%text;)*>
<!ATTLIST iframe
  src CDATA #REQUIRED
  style CDATA #IMPLIED
  scrolling CDATA #IMPLIED
  marginwidth CDATA #IMPLIED
  marginheight CDATA #IMPLIED
  frameborder CDATA #IMPLIED
  %common.att; 
>

さらに、xslt を追加しなければならない。 $FORREST_HOME/main/webapp/skins/common/xslt/html/document2html.xsl の適当なところに、次の処理を追加する。

  <xsl:template match="iframe">
    <xsl:apply-templates select="@id"/>
    <iframe>
    <xsl:copy-of select="@src"/>
    <xsl:copy-of select="@style"/>
    <xsl:copy-of select="@scrolling"/>
    <xsl:copy-of select="@marginwidth"/>
    <xsl:copy-of select="@marginheight"/>
    <xsl:copy-of select="@frameborder"/>
    </iframe>
  </xsl:template>

属性をばらばらに指定しているのは、 その順に属性を処理したいため。 アフェリエイトの提携先によっては、 属性の順番を変更するだけで規約違反になるそうなので、 注意が必要だ。

これでとりあえず iframe が通るはず。 ただし、>p< で囲むこと。 また、 forrest はドキュメントに書かれている "&amp;" という文字列を、 単独の "&" 1文字に置き換えて出力するので、 src 属性に書かれている & は &amp; にあらかじめ変換しておく必要がある。

2005-11-18

Birt の chart を使ってグラフを書く

大きくなりすぎるので、 BIRT Tips に移動。 (UNDER CONSTRUCTION)

Webページを保存する方法

Jakarta Commons に HttpClient というクラスがある。 これを使うと、Cookie の処理とか、 特に考える必要がないので便利。 基本的には、次のような使い方になる。 なお、説明のために、例外処理や、変数の宣言など、全部省略している。

    HttpClient httpClient = new HttpClient();
    Method method = new getMethod(uri); // String uri; にページの uri を指定
    httpClient.executeMethod(method);

    String responseCharSet = method.getResponseCharSet();
    InputStream is = method.getResponseBodyAsStream();

    getPage(is); // is を使ってデータを読む

    method.releaseConnection();

getPage というのは、InputStream を指定して、ページデータを読み込む処理なのだが、 byte[] へこれを読み込むとすれば、次のような処理になるだろう。

    byte[] bbuf = new byte[BBUF_SIZE];
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    bis = new BufferedInputStream(stream);

    while (true) {
        int size = bis.read(bbuf, 0, BBUF_SIZE);
        if (size < 0)
            break;
        if (size > 0) {
            baos.write(bbuf, 0, size);
        }
    }

    // 後処理とかすること

BBUF_SIZE は final static int で 1024 とか 102400 とか、 適当な値が指定されているものとする。 上記の処理は、size がずっと 0 になっていると、 永遠にループから出ることができないので、その点注意が必要かもしれない。

さて、ここで問題になるのは、 一体何を bbuf に入れたのか、ということだ。 もちろん、Webページの内容なのだが、 charset が気になるのである。 というのは、responseCharSet を全く使っていないからだ。

実はここに落とし穴がある。 responseCharSet を使ってはいけないのだ。 いけないと言い切るのも何だが、 responseCharSet が ISO-8859-1 なのに、 ページの内容は META タグで Shift_JIS が指定されている、 というようなサイトが結構ある。 そういうサイトの内容を ISO-8859-1 で保存していいのか、 という話だ。 ISO-8859-1 なら、もしかして別に構わないのかもしれないのだが、 これが EUC-JP (EUC_JP?) だったりすると、 もうこれは確実に化けまくる。 ということで、わざと charset を指定しないで、 生のままで byte[] に入れたのである。

では、こうやって入れたデータはどうやって使えばいいのか? HTML のデータを parse するには、 javax.swing.text.html.HTMLEditorKit.ParserCallback を extends したクラスを使うのが楽だ。

    Reader r = …
    ParserCallback pc = …
    boolean checkCharset = …

    javax.swing.text.html.HTMLEditorKit.Parser parser =
        new javax.swing.text.html.parser.ParserDelegator();

    parser.parse(r, pc, ignoreCharSet);

parse に指定するパラメータは、 Reader、ParserCallback、もう一つ、charset をチェックするかどうかの boolean の指定である。 最後のパラメータを true にした場合、 charSet はチェックされない。

では、charset をチェックするように指定するとどうなるか。 ここを false にすると、 charSet が予期したものでない場合には ChangedCharSetException が発生する。 このとき、 ChangedCharSetException.getCharSetSpec() を呼び出せば、charSetSpec という文字列が得られるのだが、これが例えば、

text/html; charset=Shift_JIS

みたいな文字列だから、注意が必要だ。 とにかく、これで Exception を発生することができるから、 その後は substring とかで切り出して得た charset を使って parse し直せばいい。

ArrayList から配列への変換

ArrayList の要素を要素に持つ配列を作るには、 次のメソッドを使う。

    SomeClass[] array = new SomeClass[someArrayList.size()];
    someArrayList.toArray(array);

ArrayList の要素が Object そのものの配列である場合を除いて、 次のように書くことはできない。 文法的には問題ないが、 実行時にエラーになってしまう。

    SomeClass[] array = (SomeClass[]) someArrayList.toArray();

ファイルの移動 (move)

ファイルの名前を変更する処理は File クラスの renameTo メソッドを使う。 英語は苦手なのでよく分からないのだが、なぜこのメソッドは rename ではいけないのか、いつも悩んだりするのだが、気にしない。

さて、ファイルを異なるディレクトリに移動したい場合はどうすればよいか。 これも renameTo を使うのである。つまり「名前」というのはディレクトリパスも含んでいるわけだ。

temp.txt というファイルを done ディレクトリに移動するには、次のようにすればいい。

    File fromFile = new File("temp.txt");
    File toFile = new File("done/temp.txt");
    fromFile.renameTo(toFile);

※ プログラム言語フォーラムより転載 (http://bbs.com.nifty.com/mes/cf_wrentT_m/FPL_B004/wr_type=M/wr_page=1/wr_sq=FPL_B004_0000000065)

Jakarta Commons の HttpClient で日本語のフォームを渡す

Jakarta Commons の話。@nifty の投稿フォームは MultiPart/form-data で Shift_JIS の日本語で送らなければいけないのだが、どこで指定するか?

PostMethod で getParams().setContentCharset("Shift_JIS"); を指定しただけではダメで、Part[] で指定するところの、StringPart を、

new StringPart("content", getContent(), "Shift_JIS");

のようにオブジェクトを作るのがコツらしい。getContent() は当然、Shift_JIS の文字列を返さなければならない。

プログラム言語フォーラムより転載。 (http://bbs.com.nifty.com/mes/cf_wrentT_m/FPL_B004/wr_type=M/wr_page=1/wr_sq=FPL_B004_0000000060)

jar

Java のアプリケーションを作って jar にまとめて配布する場合、java -jar を使って起動できるように、アーカイブの中にマニフェストファイルを追加する。

参考
http://java.sun.com/j2se/1.4.2/ja/runtime.html

Mawou の場合、具体的には、

jar umf Mawou.mainClass Mawou.jar forumbbs/Mawou.class

のようなコマンドを実行する。Mawou.jar が、必要なクラスをまず集めて作ったアーカイブで、Mawou.mainClass というファイルは、次の1行が入っているテキストファイル。

Main-Class: forumbbs/Mawou

Mawou が forumbbs というパッケージに入っているので、こういうパスが書いてある。ところが、これがうまく起動してくれなかった。なぜか分からなかったのだが、よく見ると、Mawou.mainClass の1行目が改行していない。

ちゃんと改行してないといけないのである。

プログラム言語フォーラムより転載 (http://bbs.com.nifty.com/mes/cf_wrentT_m/FPL_B004/wr_type=M/wr_page=1/wr_sq=FPL_B004_0000000053)

set から要素を取り出す

※注: FPL (プログラム言語フォーラム) からの転載です。 @nifty の掲示板の仕様が分かることを前提とします。

Set にはマッチした要素を取り出すメソッドはないのか? という話を書きたいのだが。

何の話かというと、Mawou の中の処理でハマったのだが、java.util.Set には、contains(Object o) というメソッドがある。指定したオブジェクトが Set に入っていたら true を返すのだ。

Mawou は、掲示板の発言を Message クラスに対応させている。

-------------------------------------------
               Message
-------------------------------------------
- messageInfo : MessageInfo
- text : String
-------------------------------------------
+ getMessageInfo() : MessageInfo
+ getParentMessageID() : int
+ setParentMessageID(id : int) : void
+ getText(): String
-------------------------------------------

Message クラスの持っている MessageInfo クラスが、発言の本文以外の情報のうち、重要なものを保持している。特に重要なのは、発言番号と日付だ。

-------------------------------------------
               MessageInfo
-------------------------------------------
- messageID : int
- parentMessageID : int
- date : Date
-------------------------------------------
+ getMessageID() : int
+ getParentMessageID() : int
+ setParentMessageID(id : int) : void
+ getDate() : Date
-------------------------------------------

さて、発言が既に取得されているかどうかを調べる場合、既に発言が入っている Map が存在して、Map のキーが MessageInfo、value が Message となっている。 但し、この Map に put するときに、

    map.put(message.getMessageInfo(), message);

のようなことをしているので、キーとなる MessageInfo は、value の持っているそれと同じオブジェクトを指していることになる。

これに対して、本文表示等で発言一覧を取得すると、新しく取得した発言に対して新しい Message オブジェクトが作られる。 これが既に取得されているかどうかは、map の中に同じ Message オブジェクトがあるかどうかで判定するのだが、「同じ」の判定はどう定義すればいいか? 既にある発言と、新規取得した発言は、オブジェクトとしては別物だが、中身が一致するはずである。

これをいちいち全メンバー変数の比較でやるのは無駄だということもあるが、例えば発言一覧で情報を取得した場合、まだ本文がない状態であっても、既に取得したことを知りたいという場合もある。そこで、一致の判定は、発言番号および発言時刻を比較し、両方が一致した場合に equals が true を返すようにしたい。

但し、どちらかの発言時刻がまだ設定されていない場合には、発言番号が一致することをもって、両者が一致したことにする。これは重要なのだが、説明は別の機会に。

発言番号だけで比較しないのは、@niftyの仕様上、会議室(掲示板)を初期化して再利用することがあるため、同じ会議室の同一発言番号の発言が複数存在することがあるから。

ということで、既に発言が入っている Map から、MessageInfo を指定して Message を取り出せるように、Comparator を作った、という話はこの前書いた。

さて、Mawou は、発言の親子情報を、ツリー表示を取得して解析する。 つまり、新規発言があったら、まず本文表示で発言を get して、その後、ツリー表示でコメント関係を get する。 そこで、Message オブジェクトは、まず本文表示のときに生成され、後から親発言を上書きする、ということになる。この処理は、こんな感じでかける。

全発言は、先ほど述べたような Map に入っている。新しく取得したツリーの情報は MessageInfo の Set を作って、そこに格納している。

    Iterator it = set.iterator();
    while (it.hasNext()) {
        MessageInfo info = (MessageInfo) it.next();
        Message messageInMap = map.get(info);
        if (messageInMap != null) {
            messageInMap.setParentMessageID(info.getParentMessageID());
        }
    }

info と messageInMap 内の MessageInfo はオブジェクトとしては別の存在だが、Comparator は番号と日付だけで判定するので、get で「一致する」から、対応する Message を取り出すことができるのだ。

ここからが本番である。Mawou は、そのときに新たに取得した発言の一覧をリストで持っている。 これは、フォーラムとか掲示板というカテゴリとは別に、最後の巡回で取得した発言一覧、というページを作りたいからだ。 ここで取得した発言は、Set に入っている。というか、入れていた。 すると、新規取得した発言に対して、次のようにしてツリー情報を追加するといいことになる。

    Iterator it = newMessageSet.iterator();

    while (it.hasNext()) {
        Message message = (Message) it.next();
        int parentMessageID = ???
        message.setParentMessageID(parentMessageID);
    }

では、ここに出てくる parentMessageID をどうすれば取得できるか? ツリー情報は Set に入っていて、この発言に対するツリー情報が入っているかどうかは、

    set.contains(message.getMessageInfo())

で調べることができる。つまり、これが true を返せば、Set の中に、同じ Message (但しオブジェクトとしては別の存在) のツリー情報が入っていることが分かる。しかし、一致しても中身は別物だということに注意。とりあえず、

    Iterator it = newMessageSet.iterator();

    while (it.hasNext()) {
        Message message = (Message) it.next();
        if (set.contains(message.getMessageInfo()) {
            int parentMessageID = ???
            message.setParentMessageID(parentMessageID);
        }
    }

あとは、set.contains でヒットした、set に入っている要素を取り出せばいいのだが、どうすればよいか。

…以上、プログラミング言語フォーラムから転載。 (http://bbs.com.nifty.com/mes/cf_wrentT_m/FPL_B004/wr_type=M/wr_page=1/wr_sq=FPL_B004_0000000025)

要するに、Set の要素を取り出すというメソッドそのものは存在しません。 もちろん、策がない訳ではなくて、 この問題の解決策として、 次のようなアドバイスがいただけました。

Setのオブジェクトをコピーして、retainAllする。

面倒といえば面倒以外の何物でもない。

toArrayした結果を自前で検索する。

こうなると何やっているかわからないというか、 最初から配列にしておけばいいような気もします。

実際には、Set ではなく Map を使うことで問題を解決しました。 つまり、set.add(object); の代わりに、map.put(object, object); を使うのです。 キーは自分自身なので、map.contains(object); で比較もできるし、 取り出しは map.get(object); で ok。

日付を表す文字列から Date オブジェクトへの変換

SimpleDateFormatを使う。

        Date date = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日(E)HH:mm");
        try {
            date = sdf.parse(dateString);
        } catch (ParseException e) { }

これで、「2005年9月20日(火)15:42」のような文字列を解析して、 date に対応した値がセットされる。

Pop before SMTP 対応サーバーを使ってメールを送信する

JavaMail API を使ってメールを送信する方法。

JavaMail

http://java.sun.com/products/javamail/index.html からダウンロードする。
現時点の最新版は、JavaMail 1.3.2 (2004-10-19)
展開したら mail.jar をクラスパスに追加する。

JAF (JavaBeans Activation Framework)

http://java.sun.com/products/javabeans/glasgow/jaf.html からダウンロードする。
現時点の最新版は JAF 1.0.2 (日付は?)
展開したら、activation.jar をクラスパスに追加する。

ポイントは、次の箇所。

        props.put("mail.smtp.auth", "true");

これによって POP before SMTP に対応している。

/*
 * Created on 2005/03/22
 */
package util;

import java.util.ArrayList;
import java.util.Properties;

import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

/**
 * @author mai
 */
public class PopBeforeSMTP {
    private String host = null; // eg. "smtp.nifty.com";

    /**
     * @return Returns the host.
     */
    public String getHost() {
        return host;
    }

    /**
     * @param host
     *            The host to set.
     */
    public void setHost(String host) {
        this.host = host;
    }

    private String POPServer = null; // eg. "pop.nifty.com";

    /**
     * @return Returns the pOPServer.
     */
    public String getPOPServer() {
        return POPServer;
    }

    /**
     * @param server
     *            The pOPServer to set.
     */
    public void setPOPServer(String server) {
        POPServer = server;
    }

    private String from = null; // eg. "SDI00000@nifty.com";

    /**
     * @return Returns the from.
     */
    public String getFrom() {
        return from;
    }

    /**
     * @param from
     *            The from to set.
     */
    public void setFrom(String from) {
        this.from = from;
    }

    private String username = null; // eg. "SDI00000";

    /**
     * @return Returns the username.
     */
    public String getUsername() {
        return username;
    }

    /**
     * @param username
     *            The username to set.
     */
    public void setUsername(String username) {
        this.username = username;

    }

    private String password = null; // eg. "myPassword";

    /**
     * @return Returns the password.
     */
    public String getPassword() {
        return password;
    }

    /**
     * @param password
     *            The password to set.
     */
    public void setPassword(String password) {
        this.password = password;
    }

    private String subject = null;

    /**
     * @return Returns the subject.
     */
    public String getSubject() {
        return subject;
    }

    /**
     * @param subject
     *            The subject to set.
     */
    public void setSubject(String subject) {
        this.subject = subject;
    }

    private String body = null;

    /**
     * @return Returns the body.
     */
    public String getBody() {
        return body;
    }

    /**
     * @param body
     *            The body to set.
     */
    public void setBody(String body) {
        this.body = body;
    }

    private ArrayList arTo = new ArrayList();

    public void setTo(String str) {
        arTo.add(str);
    }

    public String getTo() {
        return getTo(0);
    }

    public String getTo(int index) {
        if (index < 0 || index > arTo.size())
            return null;
        return (String) arTo.get(index);
    }

    public void clearTo() {
        arTo.clear();
    }

    private ArrayList arCc = new ArrayList();

    public void setCc(String str) {
        arCc.add(str);
    }

    public String getCc() {
        return getCc(0);
    }

    public String getCc(int index) {
        if (index < 0 || index > arCc.size())
            return null;
        return (String) arCc.get(index);
    }

    public void clearCc() {
        arCc.clear();
    }

    private ArrayList arBcc = new ArrayList();

    public void setBcc(String str) {
        arBcc.add(str);
    }

    public String getBcc() {
        return getBcc(0);
    }

    public String getBcc(int index) {
        if (index < 0 || index > arBcc.size())
            return null;
        return (String) arBcc.get(index);
    }

    public void clearBcc() {
        arBcc.clear();
    }

    public void clearRecipient() {
        clearTo();
        clearCc();
        clearBcc();
    }

    public boolean send() {
        if (!isParametersSet()) {
            return false;
        }

        Properties props = System.getProperties();
        props.put("mail.smtp.host", getHost());
        props.put("mail.smtp.auth", "true");

        Session session = Session.getDefaultInstance(props, null);

        MimeMessage message = new MimeMessage(session);

        try {
            message.setFrom(new InternetAddress(from));
            setRecipient(message);
            message.setSubject(getSubject());
            message.setText(getBody(), "iso-2022-jp");

            message.saveChanges();
            Transport transport = session.getTransport("smtp");
            transport.connect(getHost(), getUsername(), getPassword());
            transport.sendMessage(message, message.getAllRecipients());
            transport.close();

        } catch (javax.mail.internet.AddressException e) {
            System.out.println(e);
            return false;
        } catch (javax.mail.MessagingException e) {
            // Relay operation rejected の場合、ここに来る
            System.out.println(e);
            return false;
        }

        return true;
    }

    private boolean isParametersSet() {
        if ((host == null) || (POPServer == null) || (from == null)
                || (username == null) || (password == null)
                || (arTo.size() < 1) || (subject == null)
                || (body == null)) {

            return false;
        }

        return true;
    }

    /**
     * 受信者を設定する。
     * 
     * @param message
     * @throws javax.mail.internet.AddressException
     * @throws javax.mail.MessagingException
     */
    private void setRecipient(MimeMessage message)
            throws javax.mail.internet.AddressException,
            javax.mail.MessagingException {

        int n;
        for (n = 0; n < arTo.size(); n++) {
            message.addRecipient(Message.RecipientType.TO, new InternetAddress(
                    getTo(n)));
        }
        for (n = 0; n < arCc.size(); n++) {
            message.addRecipient(Message.RecipientType.CC, new InternetAddress(
                    getCc(n)));
        }
        for (n = 0; n < arBcc.size(); n++) {
            message.addRecipient(Message.RecipientType.BCC,
                    new InternetAddress(getBcc(n)));
        }
    }
}

※ プログラム言語フォーラムより転載 (http://bbs.com.nifty.com/mes/cf_wrentT_m/FPL_B004/wr_type=M/wr_page=1/wr_sq=FPL_B004_0000000009)