ServletのレスポンスをFilterで書き換える際、JSPが真っ白になる問題について

環境はJava6 + Tomcat7。
やりたいことはServletのFilterでレスポンスを書き換える。
ちなみにTomcatは7.0.19っす。


やー、てこずった。
レスポンスを書き換えるだけのフィルタが欲しかったんだけど、こんなのにえらくてこずってしまった。やっぱ久々だと、とにかくデバッグに時間がかかるね。


他にもいくつか問題はあったけど、結論はバッファをFlushしてないからというトホホな理由だった。

基にしたコード

まあ勉強不足なんで、適当にどこかのソースを参考にしながら作るといういつもの流れ。
最初はここを参考にした。
http://www.samuraiz.co.jp/adobeproduct/jrun/docs/jr4/docs/html/Programmers_Guide/filters6.html


上手くいかなかったので、次はここ。
http://otndnld.oracle.co.jp/document/products/ds10g/101202/doc_cd/web/B15633-02/filters.html


どっちも駄目だったけど、後者のコードをある程度残しながら作った。
正確に言うと、HTMLをフィルタリングする際はどっちも上手く行くんだ。
だがJSPの場合は真っ白。今回JSP主体のプロジェクトなので、やむを得ずコピペ作戦を諦め調査開始。

最初の問題: フィルタが複数回走る?

URLのパターンが存在しないプロジェクトだったので、こういう形で書いていたんだけど。

@WebFilter(urlPatterns="/*")
public class TranslateFilter implements Filter {

ログイン画面の表示で2回フィルタが走ってた。
二重フィルタされて最初の出力結果をロストしてんじゃないの?というのがこの時の疑問。


調べてみたら画像を読み込む際にもフィルタかかってただけだったので、面倒だしリクエストされたURLから拡張子とってはじくことにした。
ソースは後で。


この時わかったけど、1回目でフラグをオンにして2回目でそのフラグチェックするとフラグオンのまま。キープアライブ使うとフィルタのライフサイクルが複数リクエストにまたがるのかね?まあ興味ないから調べないけど。


二重にフィルタされてるんじゃなくて、ただ不要なフィルタが走ってるだけだったので、この修正後もJSP側は真っ白なままでした。

2番目の問題: JSPが相変わらず真っ白

なんか根本的に違う気がするということで、Tomcatソース落っことしてステップ実行して見ていったけど、結論から言うとTomcatさんは全然問題なかった。ちゃんと書き込んでくれていた。
Tomcatの実装でちゃんと書き込まれてるって事はこっちの問題だよなぁということで、なんか移植失敗してるのか何回も先のリンク先見たんだけど特にミスしてるような様子はない。


Tomcat側の実装でバッファのフラッシュ処理があったので、もしかしてとPrintWriterをflushさせてみたら、これだった。PrintWriterのautoFlushって引数はtrueにしてあったんだけどなぁ。釈然とせんけど、まあ調べない。
時々俺を技術者と勘違いする人がいるけど、俺はプログラムが書けるというだけで技術者ではない。技術者はこれをちゃんと調べる。あとJavaTomcatのバージョンを変えたりいろいろ検証する。まあどっかのえらい人がいつかやってくれるでしょう。

3番目の問題: 文字化け

表示されたのはされたんだけど、JSPが文字化けしてる。HTMLは正常。
JSPはPrintWriter、HTMLはOutputStreamに書いているらしいことがわかっていたので、PrintWriter側の文字コード指定ミスとあたりをつけて修正したらビンゴ。OutputStreamWriterをかましてやったらちゃんと動いた。

ソース

なんかおかしいとこあるかもしれんけど、一応動くソース。

package packJKeiri1.filter;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

@WebFilter(urlPatterns="/*")
public class TranslateFilter implements Filter {
	@Override
	public void destroy() {
	}

	@Override
	public void init(FilterConfig arg0) throws ServletException {
	}
	
	@Override
	public void doFilter(ServletRequest arg0, ServletResponse arg1,
			FilterChain arg2) throws IOException, ServletException {
		
		String ext = this.getUrlSuffix((HttpServletRequest)arg0).toLowerCase();
		if ("png".equals(ext) || "jpg".equals(ext) || "gif".equals(ext)) {
			arg2.doFilter(arg0, arg1);
			return;
		}

		GenericResponseWrapper wrapper =
				 new GenericResponseWrapper((HttpServletResponse) arg1	);
		//Note: デフォルトキャラセット。なんか他にとる方法ありそうな気もするが・・
		wrapper.setCharacterEncoding("UTF-8");
		arg2.doFilter(arg0, wrapper);
		
		arg1.setCharacterEncoding(wrapper.getCharacterEncoding());
		arg1.setContentType(wrapper.getContentType());

		OutputStream out = arg1.getOutputStream();
		byte[] by = wrapper.getData();
		if (by.length > 0) {
			int contentLength = 0;
			out.write(by);
			contentLength += by.length;
			arg1.setContentLength(contentLength);
		}
	}

	public String getUrlSuffix(HttpServletRequest req) {
		String uri = req.getRequestURI();
		if (uri == null)
			return null;
		int point = uri.lastIndexOf(".");
		if (point != -1) {
			return uri.substring(point + 1);
		}
		return uri;
	}
}

class FilterServletOutputStream extends ServletOutputStream {
	private OutputStream stream;

	public FilterServletOutputStream(OutputStream stream) {
		this.stream = stream;
	}

	@Override
	public void write(int b) throws IOException  {
		stream.write(b);
	}

	@Override
	public void write(byte[] b) throws IOException  {
		stream.write(b);
	}

	@Override
	public void write(byte[] b, int off, int len) throws IOException  {
		stream.write(b,off,len);
	}
	
	@Override
	public void close() throws java.io.IOException {
		stream.close();
	}

	@Override
	public void flush() throws IOException {
		stream.flush();
	}
}

class GenericResponseWrapper extends HttpServletResponseWrapper {
	private ByteArrayOutputStream buff;
	private PrintWriter writer;
	private ServletOutputStream output;

	public GenericResponseWrapper(HttpServletResponse response) {
		super(response);
		this.buff = new ByteArrayOutputStream();
	}

	public byte[] getData() {
		if (this.writer != null) {
			this.writer.close();
		}
		if (this.output != null) {
			try {
				this.output.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		byte[] ret = buff.toByteArray();

		this.buff = new ByteArrayOutputStream();
		this.output = null;
		this.writer = null;
		
		return ret;
	}

	@Override
	public ServletOutputStream getOutputStream() throws IOException {
		if (this.output == null)
			this.output = new FilterServletOutputStream(this.buff);

		return this.output;
	}

	@Override
	public PrintWriter getWriter() throws IOException {
		if (this.writer == null) {
			this.writer = new PrintWriter(
				new OutputStreamWriter(this.getOutputStream(),this.getCharacterEncoding()),
				true
			);
		}
		return this.writer;
	}
}

問題ある場合は誰か突っ込んでほしいっす。
問題ないものなら、誰かコピペして時間短縮できるだろうということで。

総評

こんな簡単な事でWrapperや専用のStream作ったりしないといけない上、文字化けしたりJSPとHTMLで結果違ったりして、しかもWeb上のサンプルもまともに動かないもんばっかのJavaは死ねということで。
ごめんなさい嘘です。てへっ。