/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.hayabusa.filter;

import org.opengion.fukurou.util.StringUtil;

import java.io.IOException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// import javax.servlet.annotation.WebFilter;							// 6.3.8.3 (2015/10/03)

/**
 * GZIPFilter は、Filter インターフェースを継承した ZIP圧縮クラスです。
 * web.xml で filter 設定することにより、Webアプリケーションへのアクセスを制御できます。
 * フィルタへのパラメータは、IPAddress と Debug を指定できます。
 * IPAddress は、リモートユーザーのIPアドレスをCSV形式で複数指定できます。
 * これは、CSV形式で分解した後、アクセス元のアドレスの先頭文字列の一致を
 * 判定しています。
 *
 * フィルターに対してweb.xml でパラメータを設定します。
 * <ul>
 *   <li>IPAddress :フィルタするリモートIPアドレス。無指定時は、ZIP圧縮しません。</li>
 *   <li>Debug     :フィルタ処理の詳細情報を出力します。(デバッグモード)</li>
 * </ul>
 *
 *<pre>
 * 【WEB-INF/web.xml】
 *     &lt;filter&gt;
 *         &lt;filter-name&gt;GZIPFilter&lt;/filter-name&gt;
 *         &lt;filter-class&gt;org.opengion.hayabusa.filter.GZIPFilter&lt;/filter-class&gt;
 *         &lt;init-param&gt;
 *             &lt;param-name&gt;ipAddress&lt;/param-name&gt;
 *             &lt;param-value&gt;192.168.&lt;/param-value&gt;
 *         &lt;/init-param&gt;
 *         &lt;init-param&gt;
 *             &lt;param-name&gt;debug&lt;/param-name&gt;
 *             &lt;param-value&gt;true&lt;/param-value&gt;
 *         &lt;/init-param&gt;
 *     &lt;/filter&gt;
 *
 *     &lt;filter-mapping&gt;
 *         &lt;filter-name&gt;GZIPFilter&lt;/filter-name&gt;
 *         &lt;url-pattern&gt;/jsp/*&lt;/url-pattern&gt;
 *     &lt;/filter-mapping&gt;
 *</pre>
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
// @WebFilter(filterName="GZIPFilter", urlPatterns="/jsp/*")
public class GZIPFilter implements Filter {
	private String[] ipaddrArray	;
	private boolean  isDebug		;

	/**
	 * Filter インターフェースの doFilter メソッド
	 *
	 * Filter クラスの doFilter メソッドはコンテナにより呼び出され、 最後のチェーンにおける
	 * リソースへのクライアントリクエストのために、 毎回リクエスト・レスポンスのペアが、
	 * チェーンを通して渡されます。 このメソッドに渡される FilterChain を利用して、Filter が
	 * リクエストやレスポンスをチェーン内の次のエンティティ(Filter)にリクエストとレスポンスを
	 * 渡す事ができます。
	 * このメソッドの典型的な実装は以下のようなパターンとなるでしょう。
	 * 1. リクエストの検査
	 * 2. オプションとして、入力フィルタリング用にコンテンツもしくはヘッダをフィルタリング
	 *    するためにカスタム実装によるリクエストオブジェクトのラップ
	 * 3. オプションとして、出力フィルタリング用にコンテンツもしくはヘッダをフィルタリング
	 *    するためにカスタム実装によるレスポンスオブジェクトラップ
	 * 4. 以下の a)、b) のどちらか
	 *    a) FileterChain オブジェクト(chain.doFilter()) を利用してチェーンの次のエンティティを呼び出す
	 *    b) リクエスト処理を止めるために、リクエスト・レスポンスのペアをフィルタチェーンの次の
	 *       エンティティに渡さない
	 * 5. フィルタチェーンの次のエンティティの呼び出した後、直接レスポンスのヘッダをセット
	 *
	 * @param	req		ServletRequestオブジェクト
	 * @param	res		ServletResponseオブジェクト
	 * @param	chain	FilterChainオブジェクト
	 * @throws IOException 入出力エラーが発生したとき
	 * @throws ServletException サーブレット関係のエラーが発生した場合、throw されます。
	 */
	public void doFilter( final ServletRequest req,
							final ServletResponse res,
							final FilterChain chain )
								throws IOException, ServletException {
		if( req instanceof HttpServletRequest && res instanceof HttpServletResponse ) {
			final HttpServletRequest request = (HttpServletRequest) req;
			final HttpServletResponse response = (HttpServletResponse) res;
			if( isFiltering( request ) ) {
				final GZIPResponseWrapper wrappedResponse = new GZIPResponseWrapper(response);
				chain.doFilter(req, wrappedResponse);
				wrappedResponse.finishResponse();
				return;
			}
		}
		chain.doFilter(req, res);
	}

	/**
	 * フィルター処理の判定
	 *
	 * フィルター処理を行うかどうかを判定します。
	 * ipAddress と前方一致するリモートクライアントからのアクセス時に、
	 * GZIPフィルタリング処理を行います。
	 *
	 * @param	request	HttpServletRequestオブジェクト
	 *
	 * @return	フィルター処理を行うかどうか[true:フィルタ対象/false:非対象]
	 */
	private boolean isFiltering( final HttpServletRequest request ) {

		boolean isFilter = false;

		final String ae   = request.getHeader("accept-encoding");
		final String uri  = request.getRequestURI();
		final String adrs = request.getRemoteAddr();

		// ブラウザが GZIP 対応かどうか
		if( ae != null && ae.indexOf("gzip") >= 0 ) {
			// リクエストが、jsp,js,css かどうか
			if( uri.endsWith(".jsp") || uri.endsWith(".js") || uri.endsWith(".css") ) {
				// アドレスが、指定のアドレス配列で先頭一致しているかどうか
				for( int i=0; i<ipaddrArray.length; i++ ) {
					if( adrs.startsWith( ipaddrArray[i] ) ) {
						isFilter = true;	// 一致
						break;
					}
				}
			}
		}

		if( isDebug ) {
			System.out.println("[Filtering " + isFilter + "]");
			System.out.println("  IP Address :"	+ adrs );
			System.out.println("  Request URI:"	+ uri );
		}

		return isFilter;
	}

	/**
	 * Filter インターフェースの init メソッド (何もしません)。
	 *
	 * Web コンテナは、Filter をサービス状態にするために init メソッドを呼び出します。
	 * Servlet コンテナは、Filter をインスタンス化したあと、 一度だけ init メソッドを呼び出します。
	 * Filter がフィルタリングの仕事を依頼される前に、init メソッドは正常に完了してなければいけません。
	 *
	 * init メソッドが以下のような状況になると、Web コンテナは Filter をサービス状態にできません。
	 * 1. ServletException をスローした
	 * 2. Web コネクタで定義した時間内に戻らない
	 *
	 * @param	filterConfig	FilterConfigオブジェクト
	 */
	public void init(final FilterConfig filterConfig) {
		ipaddrArray = StringUtil.csv2Array( filterConfig.getInitParameter("ipAddress") );
		isDebug = Boolean.parseBoolean( filterConfig.getInitParameter("debug") );	// 6.1.0.0 (2014/12/26) refactoring
	}

	/**
	 * Filter インターフェースの destroy メソッド (何もしません)。
	 *
	 * サービス状態を終えた事を Filter に伝えるために Web コンテナが呼び出します。
	 * Filter の doFilter メソッドが終了したか、タイムアウトに達した全てのスレッドにおいて、
	 * このメソッドを一度だけ呼び出されます。 Web コンテナがこのメソッドを呼び出した後は、
	 * Filter のこのインスタンスにおいて二度と doFilter メソッドを呼び出す事はありません。
	 *
	 * このメソッドは、フィルタに保持されている(例えば、メモリ、ファイルハンドル、スレッド)
	 * 様々なリソースを開放する機会を与え、 あらゆる永続性の状態が、メモリ上における Filter
	 * の現在の状態と同期しているように注意してください。
	 */
	public void destroy() {
		// noop
	}
}
