トップ > 教育 > 技術文書 > フィルタの続き

« フィルタ | メイン | Apache Bench(ab) の小ネタ »

フィルタの続き

先日は書いてる最中でかなりの割り込み作業が入ってきたので、中途半端になってしまいました。今日はその続きを書いておきましょう。

レスポンスフィルタ


サーブレットフィルタには、大きく、リクエストフィルタ、レスポンスフィルタ、その他があります。

よく取り上げられる、入力文字エンコードの修正フィルタはリクエストフィルタになりますね。また、ユーザーの検証を行なうフィルタもリクエストフィルタになるでしょう。

今回作成しようとしている、XSLT変換フィルタは、ここまで紹介したものとはまったく異なる、レスポンスフィルタと言うものになります。

通常のコンテンツ処理を行なって、その返ってきた内容を変更するわけですからかなり毛色が違うことがお分かりになると思います。

再度確認のために、先日示した図を再掲しておきましょう。

  • フィルタの実際 sequence2.png

この図の中で response としたパラメータが行きと帰りと二回登場していることをしっかり頭に入れておきましょう。

この部分がレスポンスフィルタの要となりますので。

では実際にどのようにして、レスポンスフィルタを構成するかを示しましょう。

  • 登場するクラス class.png

一番下に登場しているのがフィルタを実装しているクラスになります。

一番上に「ResponseWrapper」とあるのが、このフィルタから次のフィルタへと渡す「レスポンス」となります。

まだ、応答を作ってもいないのに次のフィルタにレスポンスを渡すと言われても、なんのこっちゃと思われるかと思われるかもしれませんね。

サーブレットフィルタを作成する際、通常のレスポンス(具体的な名前で行くと ServletResponse もしくは HttpServletResponse)を使いますが、これらは書き込みのみの一方通行オブジェクトです。そのため次のフィルタにレスポンスオブジェクトを渡しても、そこに何が書き込まれたのかを知ることができませんね。この問題を解決するために必要なものがこのラッパクラスになるわけです。

ResponseWrapper

では、このラッパクラスの実装を眺めてみることにしましょう。

  • ResponseWrapper
package com.dear_jpn.SimpleServlet.filters.util;
 
import java.io.IOException;
import java.io.PrintWriter;
 
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
 
import org.apache.log4j.Category;
 
public class ResponseWrapper
    extends HttpServletResponseWrapper {
 
  private ServletOutputStreamImpl sosi = null;
  private PrintWriter pw = null;
  private Category cat;
  
  public ResponseWrapper(HttpServletResponse response)
      throws IOException {
    super(response);
    cat = Category.getInstance(this.getClass());
  }
 
  public PrintWriter getWriter() throws IOException {
    if (sosi != null) {
      throw new IllegalStateException(
        "ResponseWrapper: "
        + "getWriter() has already been called "
        + "for this object");
    }
    if (pw != null) {
      return pw;
    }
 
    sosi = new ServletOutputStreamImpl();
    pw = new PrintWriter(sosi);
 
    return pw;
  }
 
  public ServletOutputStream getOutputStream()
      throws IOException {
    if (pw != null) {
      throw new IllegalStateException(
        "ResponseWrapper: "
        + "getOutputStream() has already been called "e;
        + "for this object");
    }
    if (sosi != null) {
      return sosi;
    }
 
    sosi = new ServletOutputStreamImpl();
 
    return sosi;
  }
 
  public byte[] toByteArray() {
    if (sosi != null) {
      return sosi.toByteArray();
    }
    return null;
  }
}


たぶんですけど、このクラスってば、このままで100%再利用可能なんじゃないでしょうか。

レスポンスフィルタを書く時にはまんま使ってもらってけっこうです。

んで、基本的な考え方ですが、先ほど、通常のレスポンスは書き込みのみの一方通行だと言う話をしました。これを解決する手段としてこのラッパクラスが存在すると。

で、コードは、それをそのまんま書いたと言う実装になっています。

これに後は、フィルタプログラミングでの作法に合致するような IllegalStateException の通知処理などがコードから読み取れると思います。

まずは、これ。

public class ResponseWrapper
    extends HttpServletResponseWrapper {


HttpServletResponseと言うインターフェースはかなりのメソッドを実装しなくてはなりません。

それをラッパパターンを使用して楽してしまおうと言うことです。

また、楽をするだけではなくて、自分が上位のフィルタから受け取ったレスポンスに対して、自分が変更を加えないはずの部分はそのまま下位のフィルタやメインのコンテンツに設定してもらおうと言う手法です。デザインパターンの言葉で言うと Decorator パターンと言う奴ですね。

public ResponseWrapper(HttpServletResponse response)
    throws IOException {
  super(response);
  cat = Category.getInstance(this.getClass());
}


これがコンストラクタのコード。

ここでは Decorator パターンを実現するために、継承しているラッパクラスのコンストラクタを呼び出しているだけです。

その次の Category がどうこう言うのは log4j のログをとるためのコードが紛れ込んでいるだけなので気にしなくてけっこうです。

getWriter, getOutputStream の二つのメソッドはサーブレットプログラミングで良く出てくる、バイナリデータを返すときには OutputStream で、テキストデータを返すときには PrintWriter を使うと言う奴ですね。今回のフィルタではここでオーバライドしておき、あとで何が書かれたのかを取り出せるようにするわけです。

また、サーブレットプログラミングでは、OutputStream か PrintWriter を取得した後に他方のメソッドを使うのは違反ですから、これを検出できるようなコードになっています。

このコードの中で出てくる、ServletOutputStreamImpl と言うのはまた後で説明がでてきますのでしばしお待ちを。

最後はこれ。

public byte[] toByteArray() {
  if (sosi != null) {
    return sosi.toByteArray();
  }
  return null;
}


先ほどから書いている通り、下位のフィルタで書かれた内容を取り出すのがこの部分です。

今回は、単純に書かれた内容を、バイト列でそのまま取り出すコードとしてあります。

ServletOutputStreamImpl

次は、先ほど説明を後回しにした、ServletOutputStreamImpl と言うクラスです。

package com.dear_jpn.SimpleServlet.filters.util;
 
import java.io.ByteArrayOutputStream;
import java.io.IOException;
 
import javax.servlet.ServletOutputStream;
 
public class ServletOutputStreamImpl
    extends ServletOutputStream {
 
  protected ByteArrayOutputStream baos;
 
  public ServletOutputStreamImpl() {
    baos = new ByteArrayOutputStream();
  }
 
  public void write(int data) throws IOException {
    baos.write(data);
  }
 
  public byte[] toByteArray() {
    return baos.toByteArray();
  }
 
}


ServletOutputStreamImpl クラスはServletOutputStream クラスを extends して作成しています。

ServletOutputStream クラスは abstract なクラスで、write メソッドを書いてあげなければなりません。ここでは、単純に ByteArrayOutputStream をくるんでますので、そこに処理を委譲しちゃいます。

また、何度も言っていますが、後で下位フィルタが書き込んだ内容を取り出すための仕組みとして、toByteArray なるメソッドを作成してあります。

コードもたったのこれだけです。特に詳細な説明を加えるような内容もないでしょう。

フィルタ本体

いよいよ、フィルタ本体のコードです。

フィルタコードについては、下のリンクからダウンロードしてもらって、それを参照しながら読んでもらうことにしましょう。

public class XSLTTransformFilter implements Filter {
...
}


まずは、クラス作成の際に、implements Filter として、Filter インターフェースを実装する旨宣言しています。

これは、サーブレットフィルタが同じ形でサーブレットコンテナから呼び出しを受けられるようにするために必要な手続きと言うことになります。

昨今はフレームワーク流行で、この手の書き方が浸透してきたおかげであまりこう言った部分を説明しなくて良いのは助かりますね。

次は、フィルタプログラミングでメインとなる、doFilter 処理です。

ダウンロードしてもらったコードの方には何やらいろいろなことが書かれていますが、フィルタの説明としては少々いらない部分が多くなりますので、ここで、それらの詳細を省いた部分を掲載しましょう。(コードを説明用に調整した段階でコンパイル通らなくなってるはずです。ご容赦!)

public void doFilter(ServletRequest request,
    ServletResponse response,
    FilterChain filterChain)
      throws IOException, ServletException {
 
  ResponseWrapper wrapper
    = new ResponseWrapper((HttpServletResponse)response);
 
  filterChain.doFilter(request, wrapper);
 
  byte[] xmlSource = wrapper.toByteArray();
  String xslStyleSheet = getXsltFilename();
  Source inputSource
    = new StreamSource(new ByteArrayInputStream(xmlSource));
  String url
    = getUrl((HttpServletRequest)request, xslStyleSheet);
  Source xsltSource
    = new StreamSource(new URL(url).openStream());
 
  ByteArrayOutputStream baos
    = new ByteArrayOutputStream();
  TransformerFactory factory
    = TransformerFactory.newInstance();
  Transformer transformer
    = factory.newTransformer(xsltSource);
  transformer.transform(inputSource, new StreamResult(baos));
 
  byte[] resultHTML = baos.toByteArray();
  response.setContentLength(resultHTML.length);
  response.setContentType("text/html");
  response.setCharacterEncoding("UTF-8");
 
  ServletOutputStream out = response.getOutputStream();
  out.write(resultHTML);
 
}


では、これを上から見ていくことにしましょう。

まずは、いきなり ResponseWrapper の作成からはじまります。

ここで、自分が受け取ったレスポンス(変な言い方ですが、誤解してませんね!?)をもとにして、自分自身が下位のフィルタから返してもらうレスポンスを作っているわけです。

当然、ここで作った段階では空っぽの状態のレスポンスですから、これを下位のフィルタに渡して、ここにコンテンツを書いてもらうことになります。

それを実現するために、

filterChain.doFilter(request, wrapper);


として、単純に下位のフィルタを呼び出す doFilter 呼び出しを行なっています。

この呼び出しが帰ってきた段階で、すでに wrapper オブジェクトの中にはxmlファイルなどのコンテンツが返ってきていることになるわけです。

引き続き、この返ってきた wrapper オブジェクトを元に、変換作業を進めることになります。

この変換作業に関しては、先日説明した範囲ですからここでは繰り返しません。

変換作業とは別にフィルタの動作として重要になるのが、次の部分です。

response.setContentLength(resultHTML.length);
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");


setContentLength にて、ヘッダを更新しています。

当然ながら、下位のフィルタが書き込んできた長さとこのフィルタが変換した長さは異なります。ここを正しく設定しないと特定のブラウザでは動作するのに別のブラウザでは動かないことがあると言ったことになりかねません。

また、下位のフィルタ経由で受け取った ContentType は text/xml ですが、変換後処理して欲しいのは html ですから、これも text/html に変換してあげます。

また、ここでは処理しませんでしたが、返す際に文字コードの変換などを行なうのであれば、setCharacterEncoding も変えてあげる必要があるでしょう。

おおよそ、このあたりのヘッダをいじってあげれば、たいていの場合において問題なく動作するはずです。

ServletOutputStream out = response.getOutputStream();
out.write(resultHTML);


最後が、最終的にこのフィルタの出力を確定させる処理です。

この部分はサーブレットの書き方と同じですから、特に説明を加える必要もないでしょう。

おしまい

これで一通りレスポンスフィルタに関する話はおしまいです。

思っていた以上に書かねばならないことが多かったですね。

毎回この手のフィルタを書くときには、自分が過去に書いたコードを探しまわしてたんですから、ここに書いておけば、その手間がはぶけるでしょう。

ついでに自分で一通り知識を整理してみたら、意外にわかってたつもりで分かってなかった部分があったことに気がつきまして、まとめた価値はあったなと言うところです。

アーカイブ