ログアウト||セッションタイムアウト時に処理したい


どっかにオーバーライドする場所あるかねえとかWebSessionとかのソースたどり始めて、ふと思う。
これで動くんじゃね?

public class TestSession extends WebSession implements HttpSessionBindingListener {

    private static final long serialVersionUID = 1L;

    protected TestSession(Request request){
        super(request);
    }

    @Override
    public void valueBound(HttpSessionBindingEvent event) {
        System.out.println("**** セッション生成 ****");
    }

    @Override
    public void valueUnbound(HttpSessionBindingEvent event) {
        System.out.println("**** セッション破棄 ****");
    }
}

HttpSessionBindingListenerていうのは、あれです。セッションが生成/破棄されたとき通知してくれるやつ。
動きました。そりゃそうだセッションオブジェクトだもの。

追記:
これ注意が必要。具体的に言うと、セッションタイムアウトのタイミングだと、Session.getApplication()がExceptionを返します。
getApplication()はThreadLocalに格納されたアプリケーションオブジェクトを取得する仕組みだから、バックグラウンドプロセスからは自アプリケーションを取得できないんですね。どうしたものか。

AbstractChoiceでnullを選択肢としてつかう

まあ、そんな設計まずいかんだろという話はおいといて。

RadioChoice<Boolean> choices = 
    new RadioChoice<Boolean>("wicket:id",Arrays.asList(null,true,false));
choices.setChoiceRenderer(new IChoiceRenderer<Boolean>(){
    private static final long serialVersionUID = 1L;
    @Override
    public Object getDisplayValue(Boolean value) {
        if(value==null){
            return "どちらともいえない日本人的回答";
        }
        return value?"はい":"いいえ";
    }
    @Override
    public String getIdValue(Boolean object, int index) {
        if(object==null){
            return "-1";//AbstractSingleSelectChoice.NO_SELECTION_VALUE
        }else{
            return object.toString();
        }
    }
});
//関係ないけど改行しない場合こんなかんじだよね
choices.setSuffix("&nbsp;&nbsp;");

NO_SELECTION_VALUEがprotectedなので、ちょっといやんなかんじ。
まあ、どんな状況でも未選択を残したい場合とか、プロパティファイルの"nullValid"がアレな場合とかに。

Google Protocol Buffersしてみた

これ Developer Guide  |  Protocol Buffers  |  Google Developers

まずは、GitHub - protocolbuffers/protobuf: Protocol Buffers - Google's data interchange formatからコンパイラとソースをダウンロード。
で、protoc.exeを「protobuf-2.0.0beta/src」にコピーして 「protobuf-2.0.0beta/java/README.txt」の記述どおりにMavenでごにょごにょっとjarを作る。

次にXMLでいうDTDみたいなものを作成。こんなかんじ。とりあえず、ファイル名は「rss_test.proto」としました。

package prototest;

option java_package ="koyane.prototest";
option java_outer_classname = "RssTest";

message Item {
  required string title = 1;
  required string link = 2;
  optional string description =3;
  required string creator = 4;
  required string date = 5;
  optional string subject = 6;

}

message RssChannel {
  required string title = 1;
  required string link = 2;
  optional string description =3;

  repeated Item items =4;
}

なんとなーくjavaとかC++ぽい表記。リファレンスよまなくても何だかわかりそう。雰囲気で。

「option java_package」は生成されるJavaクラスのパッケージ。指定されてなければ「package」の記述が適用されるらしい。

「option java_outer_classname」は生成されるJavaクラスの名称。指定されてなければファイル名がクラスの名前に使われるみたい。この例でいくと、「rss_test.proto」だから、「RssTest」みたいな。

message XXX{
 ...
}

の部分は生成されるJavaクラスのインナークラスに該当してました。
この例ではstringしか使ってないけどint32(int)とかint64(long)とかbool(boolean)とかあります。

required は必須フィールド
optional は必須じゃないフィールド
repeated はまあListになる感じ

試してないけどmessageを宣言する順序は関係ありそう。サンプル見る限り。

で、.protoファイルを作成したら、コンパイラ protoc.exe の出番

protoc --java_out=/ rss_test.proto

でとりあえず、ファイルシステムのルートにJavaソースコードが生成されます。この例だと、koyane.prototest.RssTest.java
ふー。やっと準備完了。ここからが読み書きです。

まずはインスタンスを生成してファイルに保存

package koyane.prototest;

import java.io.FileOutputStream;
import java.util.Iterator;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import koyane.prototest.RssTest.Item;
import koyane.prototest.RssTest.RssChannel;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class TestWrite {
    public static void main(String[] args) throws Exception{
        Document doc = read("http://d.hatena.ne.jp/koyane/rss");
        Element root = doc.getDocumentElement();
        XPathFactory factory = XPathFactory.newInstance();
        XPath xpath = factory.newXPath();
        xpath.setNamespaceContext(new NamespaceContext(){
            @Override
            public String getNamespaceURI(String prefix) {
                if(prefix.equals("dc")){
                    return "http://purl.org/dc/elements/1.1/";
                }else{
                    return XMLConstants.NULL_NS_URI;
                }
            }
            @Override
            public String getPrefix(String namespaceURI) {
                throw new UnsupportedOperationException();
            }
            @Override
            public Iterator<?> getPrefixes(String namespaceURI) {
                throw new UnsupportedOperationException();
            }
        });
        Node cNode = (Node)xpath.evaluate("channel", root, XPathConstants.NODE);
        
        //ここ。ここがProtocol Buffers。
        RssChannel.Builder channel = RssChannel.newBuilder();
        channel.setTitle(xpath.evaluate("title/text()", cNode));
        channel.setLink(xpath.evaluate("link/text()", cNode));
        channel.setDescription(xpath.evaluate("description/text()", cNode));

        NodeList iList = (NodeList)xpath.evaluate("item", root, XPathConstants.NODESET);

        for (int i=0;i<iList.getLength();i++) {
            Node iNode = iList.item(i);
            //あとここ。ここがProtocol Buffers。
            Item.Builder item = Item.newBuilder();
            item.setTitle(xpath.evaluate("title/text()", iNode));
            item.setLink(xpath.evaluate("link/text()", iNode));
            item.setDescription(xpath.evaluate("description/text()", iNode));
            item.setCreator(xpath.evaluate("creator/text()", iNode));
            item.setDate(xpath.evaluate("date/text()", iNode));
            item.setSubject(xpath.evaluate("subject/text()", iNode));
            
            channel.addItems(item.build());
        }
        //で、ファイルに書き出し。
        FileOutputStream output = new FileOutputStream("c:/rss_test_result");
        channel.build().writeTo(output);
        output.close();
    }
    
    private static Document read(String url) throws Exception{
        HttpClient httpclient = new HttpClient();
        GetMethod method = new GetMethod(url);
        httpclient.executeMethod(method);
        
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document doc = builder.parse(method.getResponseBodyAsStream());

        method.releaseConnection();
        
        return doc;
    }
}

うりゃーーーっと、はてなRSSを読み込んで保存。ひでーこーどだ。Protocol Buffersのところは超簡単。

次にこのファイルを復元。

package koyane.prototest;

import java.io.FileInputStream;

import koyane.prototest.RssTest.Item;
import koyane.prototest.RssTest.RssChannel;

public class TestRead {
    public static void main(String[] args) throws Exception{
        RssChannel channel = RssChannel.parseFrom(new FileInputStream("c:/rss_test_result"));
        System.out.println("title : " + channel.getTitle());
        System.out.println("link : " + channel.getLink());
        System.out.println("description : " + channel.getDescription());
        
        for (Item item : channel.getItemsList()) {
            System.out.println();
            System.out.println("title : " + item.getTitle());
            System.out.println("link : " + item.getLink());
            System.out.println("description : " + item.getDescription());
            System.out.println("creator : " + item.getCreator());
            System.out.println("date : " + item.getDate());
            System.out.println("subject : " + item.getSubject());
        }
    }
}

おお、できた。
今のところ私に使い道はないけど、面白いなあ。

ファイルダウンロード テキストファイルの場合

ついでにこっちも。矢野さんのサイトで細かく紹介しているから不要なエントリーともいう。
http://www.javelindev.jp/wicket/doc/tutorial02#i7

同じじゃ芸がないからちょっとちがう所をオーバーライド。まあやってることは同じなんだけれど。

StringBufferResourceStream stream = 
  new StringBufferResourceStream("application/octet-stream"){
    private static final long serialVersionUID = 1L;
    @Override
    public long length() {
        return asString().getBytes(getCharset()).length;
    }
};
stream.setCharset(Charset.forName("Windows-31J"));
stream.append(...);//文字列書き出しする
IRequestTarget target = new ResourceStreamRequestTarget(stream,"text.xls");
getRequestCycle().setRequestTarget(target );

1.4m2でもStringBufferResourceStreamはマルチバイト対応はしていませんでした。
まあバグといえばバグなんだろうけど、JavaDocにマルチバイト対応してないよ、と記述しておけばいいレベルなのかなあと思わなくもない。

ファイルダウンロード OutputStreamに直接書き出したい場合

まあ調べればすぐわかるんだけれども、日本語の情報はなかったようなので。
業務で必要になった、POIで処理したExcelをダウンロードするボタン。
例外処理とかてけとー。

//請求書ダウンロードボタン
Button<Void> downloadButton = new Button<Void>("downloadButton"){
  private static final long serialVersionUID = 1L;
  @Override
  public void onSubmit() {
    IResourceStream stream = new AbstractResourceStreamWriter(){
      private static final long serialVersionUID = 1L;
      @Override
      public void write(OutputStream output) {
        try {
          HSSFWorkbook workbook = //ここでExcel編集とかなんとか
          workbook.write(output);//適当に書き出す
        } catch (Exception e) {
          // TODO 例外処理
          e.printStackTrace();
        }
      }
      @Override
      public String getContentType() {
        return "application/vnd.ms-excel";
      }
    };
    //TODO ダウンロードファイル名
    IRequestTarget target = new ResourceStreamRequestTarget(stream,"text.xls");
    getRequestCycle().setRequestTarget(target);
  }
};

お手軽。さすがWicket

Integerを頭ゼロ埋めで表示するラベルを作る

public class CodeLabel extends Label<Integer> {
    private static final long serialVersionUID = 1L;
    private int zeroPadLength;
    public CodeLabel(String id, int zeroPadLength) {
        super(id);
        this.zeroPadLength = zeroPadLength;
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public <T> IConverter<T> getConverter(Class<T> type) {
        if(type == Integer.class){
            return new ZeroPaddingIntegerConverter(zeroPadLength);
        }
        return super.getConverter(type);
    }
}

無名クラスでやってもいいけど、何度も出てきそうなので、クラスにしてみる。Generics使ったときのgetConverter()の書き方がいまいちわからんなあ。

追記:変だと思っていたらm2でここらかわってました。

public class CodeLabel extends Label<Integer> {
    private static final long serialVersionUID = 1L;
    private int zeroPadLength;
    public CodeLabel(String id, int zeroPadLength) {
        super(id);
        this.zeroPadLength = zeroPadLength;
    }
    
    @Override
    public IConverter<Integer> getConverter(Class<Integer> type) {
        return new ZeroPaddingIntegerConverter(zeroPadLength);
    }
}

まあそうだよね。納得。

Formの継承クラスを作成せずにエラーメッセージを変える

結局つかわなかったのだけれど、調べたので。

Form<UserBean> userForm = new Form<UserBean>("userForm"){
    private static final long serialVersionUID = 1L;
    @Override
    public String getValidatorKeyPrefix() {
        return "ユーザー";
    }
};

TextField<String> userId = new TextField<String>("id");
userId.setRequired(true);
userId.add(new PatternValidator("^[0-9A-Za-z]*$"));
userForm.add(userId);

TextField<String> kana = new TextField<String>("kana");
kana.setRequired(true);
kana.add(new PatternValidator("^[ア-ン]*$"));
userForm.add(kana);
...

getValidatorKeyPrefixというメソッドをオーバーライドする。元はnullを返してるだけなので、単に好きな文字列返してみる。
プロパティファイルはこんな

ユーザーRequired="${label}"を入力しろやゴラ

ユーザーid=ユーザーID
ユーザーid.PatternValidator="${label}"は半角英数じゃゴラ

ユーザーkana=ユーザ名フリガナ
ユーザーkana.PatternValidator="${label}"は全角カタカナじゃゴラ

これでパッケージを除くFormクラス名の代わりにPrefixの文字列が使われる感じ。だいたい。
まあ厳密にいうとプロパティファイルのキーの指定ルールとか変にわかりにくいのだけれど、エラーメッセージを参照するとわかります。となげだす。