/*
 * 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.taglib;

import org.opengion.fukurou.system.OgRuntimeException ;		// 6.4.2.0 (2016/01/29)
import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
// import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.ToString;						// 6.1.1.0 (2015/01/17)
import static org.opengion.fukurou.util.StringUtil.nval ;

import org.apache.commons.codec.binary.Base64 ;
import java.nio.charset.Charset;								// 5.5.2.6 (2012/05/25)

/**
 * Cookie を読み書きするタグです。
 *
 * Cookie は少量の情報を Servlet から Web ブラウザに送り、 ブラウザにそれを
 * 維持しもらい、以降のアクセスでサーバに送り返してもらう仕組です。
 * Cookie の値はクライアントを一意に識別できるようになっているので一般に
 * セッション管理に用いられています。
 *
 * Cookie には名前と値が一つありますが、他にコメントやパス、ドメイン、
 * 最長存続期間、バージョンといったオプショナルな属性もあります。
 * Web ブラウザの中にはオプショナルな属性の扱いにバグがあるものがあります。
 * このため、Servlet の相互運用性を高めるためにはあまり使わないほうがいいでしょう。
 *
 * 標準の JavaScript で登録機能はサポートしていましたが、メモリのみで、かつ
 * 画面単位の書き込みのみでした。
 * 今回の cookie タグでは、永続化(maxAge)の設定や、システム内(CONTEXT_NAME以下)
 * での共有(デフォルト)や、その変更、ドメインを指定しての共有(domain)などの
 * 機能を持っています。
 * また、漢字コードでの読み書き(useBase64)にも対応しています。
 * 読み込みに関しては、漢字を指定しなければ、{&#064;SYS.COOKIE.カラム名}で、使用可能です。
 * 複数の読み込み、また、漢字コードを含むクッキーの場合は、読み込み(action="LOAD")
 * してください。指定のキー以外に、別名に読み込む(aliasNames)事も可能です。
 *
 * @og.formSample
 * ●形式：
 *    &lt;og:cookie
 *        action    = "SAVE"       Cookie に対するアクションを指定します。(SAVE|LOAD|DELETE)
 *        keys      = "AAA,BBB"    キーをCSV形式で複数指定できます。
 *        vals      = "VAL1,VAL2"  値をCSV形式で複数指定できます。
 *        path      = "/ge"        クライアントがこの Cookie を返さなくてはいけないパスを指定します。
 *        domain    = ".foo.com"   この Cookie がどこで生成されたかを表すドメインを指定します。
 *        maxAge    = "3600"       Cookie の最長存続期間を秒単位で設定します。
 *        useBase64 = "false"      漢字等の２Byte文字を使用する場合に、BASE64で処理します。[true/false]
 *    &gt;
 * ●body：なし
 *
 * ●Tag定義：
 *   &lt;og:cookie
 *       action           ○【TAG】アクション(SAVE,LOAD,DELETE)をセットします(必須)。
 *       keys             ○【TAG】クッキーのキーをCSV形式で複数指定します(必須)。
 *       vals               【TAG】keys属性に対応する値をCSV形式で複数指定します
 *       aliasNames         【TAG】クッキーのキーの別名をCSV形式で複数指定します
 *       path               【TAG】クライアントがこの Cookie を返さなくてはいけないパスを指定します(初期値:/+CONTEXT_NAME)
 *       domain             【TAG】この Cookie がどこで生成されたかを表すドメインを指定します(初期値:付与したサーバ)
 *       maxAge             【TAG】Cookie の最長存続期間を秒単位で設定します(初期値: -1 )
 *       useBase64          【TAG】漢字等の文字を扱う場合に、BASE64で処理を行うかどうか[true/false]を設定します(初期値:false )
 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
 *   /&gt;
 *
 * ●使用例
 *    例１)設定:複数キーを同時に書き込むことが可能です。
 *        &lt;og:cookie
 *            action="SAVE" keys="CDJ,FG_NAME" vals="{&#064;CDJ},{&#064;NAME}"
 *        /&gt;
 *
 *    例２)取得:cookieタグで取得すると、それ以降では {&#064;CDJ} や {&#064;NAME} で扱えます。
 *          aliasNames 属性を使わない場合は、keys に指定した変数にセットされます。
 *        &lt;og:cookie
 *            action="LOAD" keys="CDJ,FG_NAME" aliasNames="CDJ,NAME"
 *        /&gt;
 *
 *    例３)取得:SYS パラメータでの取得も可能です。
 *        {&#064;SYS.COOKIE.CDJ}
 *
 *    例４) QUERY画面では値の表示(LOAD)を行い、RESULT画面で値の設定(SAVE)を行うケース
 *
 *        QUERY画面
 *        &lt;og:cookie action="LOAD" useBase64="true"
 *                        keys="CLM,NAME" aliasNames="CLM,LABEL_NAME" /&gt;
 *
 *        &lt;og:column name="CLM"        defaultVal="{&#064;CLM}" /&gt;
 *        &lt;og:column name="LABEL_NAME" defaultVal="{&#064;LABEL_NAME}"/&gt;
 *
 *        RESULT画面
 *        &lt;og:cookie action="SAVE" maxAge="360000" useBase64="true"
 *                        keys="CLM,NAME" vals="{&#064;CLM},{&#064;LABEL_NAME}" /&gt;
 *
 *    例５) QUERY画面では、{&#064;SYS.COOKIE.カラム名} で取得。
 *        RESULT画面では、ムラタ内全システム共通に使える値をセット。
 *
 *        QUERY画面
 *        &lt;og:column name="SYSTEM_ID" defaultVal="{&#064;SYS.COOKIE.SYSTEM_ID}" /&gt;
 *
 *        RESULT画面
 *        &lt;og:cookie action="SAVE" maxAge="360000" domain=".opengion.org"
 *                        keys="SYSTEM_ID" vals="{&#064;SYSTEM_ID}" /&gt;
 *
 * @og.rev 3.8.0.2 (2005/06/30) 新規作成
 * @og.group 画面制御
 *
 * @version  0.9.0  2000/10/17
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class CookieTag extends CommonTagSupport {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.4.2.0 (2016/01/29)" ;
	private static final long serialVersionUID = 642020160129L ;

	/**
	 * プラットフォーム依存のデフォルトの Charset です。
	 * プラットフォーム依存性を考慮する場合、エンコード指定で作成しておく事をお勧めします。
	 *
	 * @og.rev 5.5.2.6 (2012/05/25) findbugs対応
	 */
	private static final Charset DEFAULT_CHARSET = Charset.defaultCharset() ;

	/** action 引数に渡す事の出来る アクション  設定 {@value} */
	public static final String ACT_SAVE = "SAVE" ;
	/** action 引数に渡す事の出来る アクション  取得 {@value} */
	public static final String ACT_LOAD = "LOAD" ;
	/** action 引数に渡す事の出来る アクション  削除 {@value} */
	public static final String ACT_DELETE = "DELETE" ;

	/** action 引数に渡す事の出来る アクション リスト  */
	private static final String[] ACTION_LIST = { ACT_SAVE , ACT_LOAD , ACT_DELETE };

	private String   action		;
	private String[] keys		;
	private String[] vals		;
	private String[] aliasNames	;
	private String   path		= "/" + HybsSystem.sys( "CONTEXT_NAME" );
	private String   domain		;
	private int      maxAge		= -1;
	private boolean  useBase64	;

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public CookieTag() {
		super();		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
	}

	/**
	 * Taglibの終了タグが見つかったときに処理する doEndTag() を オーバーライドします。
	 *
	 * @return	後続処理の指示(EVAL_PAGE)
	 */
	@Override
	public int doEndTag() {
		debugPrint();		// 4.0.0 (2005/02/28)
		actionExec( action );

		return EVAL_PAGE ;
	}

	/**
	 * タグリブオブジェクトをリリースします。
	 * キャッシュされて再利用されるので、フィールドの初期設定を行います。
	 *
	 */
	@Override
	protected void release2() {
		super.release2();
		action		= null;
		keys		= null;
		vals		= null;
		aliasNames	= null;
		path		= "/" + HybsSystem.sys( "CONTEXT_NAME" );
		domain		= null;
		maxAge		= -1;
		useBase64	= false;
	}

	/**
	 * アクションを実行します。
	 *
	 * アクションは action 属性で指定します。
	 *
	 * @og.rev 6.3.4.0 (2015/08/01) Arrays.toString から String.join に置き換え。
	 *
	 * @param	action	アクション (public static final 宣言されている文字列)
	 */
	private void actionExec( final String action ) {

		if( ACT_SAVE.equals( action ) ) {
			saveCookies( maxAge );
		}
		else if( ACT_LOAD.equals( action ) ) {
			loadCookies();
		}
		else if( ACT_DELETE.equals( action ) ) {
			saveCookies( 0 );	// maxAge にゼロをセットすると削除されます。
		}
		else {
			final String errMsg = "指定のアクションは実行できません。アクションエラー"	+ CR
							+ "action=[" + action + "] " 								+ CR
//							+ StringUtil.array2csv( ACTION_LIST ) ;
							+ "actionList=" + String.join( ", " , ACTION_LIST ) ;
			throw new HybsSystemException( errMsg );
		}
	}

	/**
	 * SAVE アクションを実行します。
	 *
	 * クッキーに書き込みを行います。
	 *
	 * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
	 *
	 * @param	maxAge	最長存続期間 ( 0 なら削除 )
	 */
	private void saveCookies( final int maxAge ) {
		// 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
		if( keys == null || vals == null || keys.length != vals.length ) {
			final String errMsg = "キーが未登録か、値が未登録か、キーと値の個数が異なります。" ;
			throw new OgRuntimeException( errMsg );
		}

	//	BASE64Encoder encoder = null;
		for( int i=0; i<keys.length; i++ ) {
			String value = nval (vals[i],"" );
			if( useBase64 && value.length() > 0 ) {
	//			if( encoder == null ) { encoder = new BASE64Encoder(); }
	//			value = encoder.encode( value.getBytes() ) ;

				final byte[] encoded = Base64.encodeBase64( value.getBytes( DEFAULT_CHARSET ) );		// 5.5.2.6 (2012/05/25) findbugs対応
				value = new String( encoded,DEFAULT_CHARSET );									// 5.5.2.6 (2012/05/25) findbugs対応
			}
			setCookie( keys[i], value, maxAge );
		}
	}

	/**
	 * LOAD アクションを実行します。
	 *
	 * クッキーから読み込みを行います。
	 *
	 * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
	 *
	 */
	private void loadCookies() {
		// 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
		if( keys == null ) {
			final String errMsg = "キーが未登録です。" ;
			throw new OgRuntimeException( errMsg );
		}

	//	BASE64Decoder decoder = null;
		if( aliasNames == null ) { aliasNames = keys; }

	//	try {
			for( int i=0; i<keys.length; i++ ) {
				String value = getCookie( keys[i] );
				if( useBase64 && value != null && value.length() > 0 ) {
	//				if( decoder == null ) { decoder = new BASE64Decoder(); }
	//				byte[] decoded = decoder.decodeBuffer( value );
					final byte[] decoded = Base64.decodeBase64( value.getBytes( DEFAULT_CHARSET ) );		// 5.5.2.6 (2012/05/25) findbugs対応
					value = new String( decoded,DEFAULT_CHARSET );									// 5.5.2.6 (2012/05/25) findbugs対応
				}
				if( value != null ) {
					setRequestAttribute( aliasNames[i],value );
				}
			}
	//	}
	//	catch( IOException ex ) {
	//	final String errMsg = "BASE64Decoder エラー " + ex.toString();
	//		throw new HybsSystemException( errMsg );
	//	}
	}

	/**
	 * 【TAG】アクション(SAVE,LOAD,DELETE)をセットします。
	 *
	 * @og.tag
	 * アクションは,HTMLから(get/post)指定されますので,ACT_xxx で設定される
	 * フィールド定数値のいづれかを、指定できます。
	 * 無指定の場合は、なにもしません。
	 *
	 * <table border="1" frame="box" rules="all" >
	 *   <caption>アクションの説明</caption>
	 *   <tr><th>action </th><th>名称</th><th>機能</th></tr>
	 *   <tr><td>SAVE   </td><td>登録</td><td>指定の keys のキーに vals の値をセットします。</td></tr>
	 *   <tr><td>LOAD   </td><td>取得</td><td>指定の keys のクッキーを(リクエスト中に)取得します。</td></tr>
	 *   <tr><td>DELETE </td><td>削除</td><td>指定の keys のクッキーを削除します。</td></tr>
	 * </table>
	 *
	 * @og.rev 6.3.4.0 (2015/08/01) Arrays.toString から String.join に置き換え。
	 *
	 * @param	act アクション (public static final 宣言されている文字列)
	 * @see		<a href="../../../../constant-values.html#org.opengion.hayabusa.taglib.CookieTag.ACT_DELETE">アクション定数</a>
	 */
	public void setAction( final String act ) {
		action = nval( getRequestParameter( act ),action );

		if( action != null && !check( action, ACTION_LIST ) ) {
			final String errMsg = "指定のアクションは実行できません。アクションエラー"	+ CR
							+ "action=[" + action + "] "								+ CR
//							+ StringUtil.array2csv( ACTION_LIST ) ;
							+ "actionList=" + String.join( ", " , ACTION_LIST ) ;
			throw new HybsSystemException( errMsg );
		}
	}

	/**
	 * 【TAG】クッキーのキーをCSV形式で複数指定します。
	 *
	 * @og.tag
	 * クッキーにセットするときのキーを指定します。CSV形式で複数指定できます。
	 * vals 属性には、キーに対応する値を、設定してください。
	 * 分解方法は、CSV変数を先に分解してから、getRequestParameter で値を取得します。
	 * こうしないとデータ自身にカンマを持っている場合に分解をミスる為です。
	 *
	 * @param	key	クッキーのキー
	 */
	public void setKeys( final String key ) {
		keys = getCSVParameter( key );
	}

	/**
	 * 【TAG】クッキーのキーの別名をCSV形式で複数指定します。
	 *
	 * @og.tag
	 * クッキーから値を取得する(action="LOAD")場合に、読み込みキー(keys)に対応する
	 * 別名を指定することで、別名の変数に読み込んだ値を登録することが出来ます。
	 * 別名を指定しない場合は、keys に指定された名前が、使用されます。
	 * 分解方法は、CSV変数を先に分解してから、getRequestParameter で値を取得します。
	 * こうしないとデータ自身にカンマを持っている場合に分解をミスる為です。
	 *
	 * @param	names	クッキーの別名
	 */
	public void setAliasNames( final String names ) {
		aliasNames = getCSVParameter( names );
	}

	/**
	 * 【TAG】keys属性に対応する値をCSV形式で複数指定します。
	 *
	 * @og.tag
	 * キーに設定した値を、CSV形式で複数して出来ます。
	 * 指定順序は、キーと同じにしておいて下さい。
	 * 分解方法は、CSV変数を先に分解してから、getRequestParameter で値を取得します。
	 * こうしないとデータ自身にカンマを持っている場合に分解をミスる為です。
	 *
	 * @param	val	keys属性に対応する値
	 */
	public void setVals( final String val ) {
		vals = getCSVParameter( val );
	}

	/**
	 * 【TAG】クライアントがこの Cookie を返さなくてはいけないパスを指定します(初期値:/+CONTEXT_NAME)。
	 *
	 * @og.tag
	 * この値を指定すると Cookie が該当するディレクトリ内、さらに、
	 * サブディレクトリに存在する全てのページから参照できるようになります。
	 * Cookie のパスには Cookie をセットした Servlet が含まれていなければなりません。
	 * 例えば、/catalog を指定したとします。このとき、 サーバの /catalog 以下の全ての
	 * ディレクトリから Cookie が見えるようになります。
	 * 初期値は、"/" + CONTEXT_NAME です。
	 *
	 * Cookie のパス名指定についての詳細は RFC2109 を参照してください。
	 *
	 * @param	uri パスを表すURL
	 */
	public void setPath( final String uri ) {
		path = nval( getRequestParameter( uri ),path );
	}

	/**
	 * 【TAG】この Cookie がどこで生成されたかを表すドメインを指定します(初期値:付与したサーバ)。
	 *
	 * @og.tag
	 * ドメイン名の形式は RFC2109 で指定されています。
	 * ドメイン名は (.foo.com のように) ドットで始まります。 このように設定すると、
	 * Cookie は指定された Domain Name System (DNS) のゾーン内のサーバから見える
	 * ようになります(例えば、www.foo.com) からは見えるけれど、a.b.foo.com からは
	 * 見えないというようにです)。 デフォルトでは Cookie を付与したサーバにしか送り返しません。
	 *
	 * @param	pattern 生成ドメイン名
	 */
	public void setDomain( final String pattern ) {
		domain = nval( getRequestParameter( pattern ),domain );
	}

	/**
	 * 【TAG】Cookie の最長存続期間を秒単位で設定します(初期値: -1 )。
	 *
	 * @og.tag
	 * 正の値が指定されると Cookie はある秒数が過ぎた後、削除されます。
	 * この値は、Cookie の有効期限が切れる 最長 存続期間であることに注意してください。
	 * Cookie の現在までの存続期間ではありません。
	 *
	 * 負の値は Cookie が永続的に保存されないことを意味しています。 この場合、
	 * Web ブラウザが終了すると Cookie も削除されます。 0 という値を指定すると
	 * Cookie が削除されることになります。
	 * 初期値は、-1(永続的に保存されない)です。
	 *
	 * @param	expiry 最長存続期間(秒) (負の値は Cookie を保存しない、 0 なら Cookie を削除する意味となる)
	 */
	public void setMaxAge( final String expiry ) {
		maxAge = nval( getRequestParameter( expiry ),maxAge );
	}

	/**
	 * 【TAG】漢字等の文字を扱う場合に、BASE64で処理を行うかどうか[true/false]を設定します(初期値:false )。
	 *
	 * @og.tag
	 * クッキーへの読み書きは、ASCII に限られます。漢字等のコードを書き込む場合は、
	 * BASE64でエンコードして書き込む必要があります。読み込む場合も同様です。
	 * ただし、一般のASCIIは、BASE64 ではエンコードしないため、外部で指定する必要があります。
	 * BASE64 で書き込んだ場合ば、{&#064;SYS.COOKIE.CDJ} での取得はできませんので、
	 * action="LOAD" で、取得してください。
	 * 初期値は、false(使用しない)です。
	 *
	 * @param	flag BASE64処理可否 [true:する/false:しない]
	 */
	public void setUseBase64( final String flag ) {
		useBase64 = nval( getRequestParameter( flag ),useBase64 );
	}

	/**
	 * このオブジェクトの文字列表現を返します。
	 * 基本的にデバッグ目的に使用します。
	 *
	 * @return このクラスの文字列表現
	 * @og.rtnNotNull
	 */
	@Override
	public String toString() {
		return ToString.title( this.getClass().getName() )
				.println( "VERSION"		,VERSION	)
				.println( "action"		,action		)
				.println( "keys"		,keys		)
				.println( "vals"		,vals		)
				.println( "aliasNames"	,aliasNames	)
				.println( "path"		,path		)
				.println( "domain"		,domain		)
				.println( "maxAge"		,maxAge		)
				.println( "useBase64"	,useBase64	)
				.println( "Other..."	,getAttributes().getAttribute() )
				.fixForm().toString() ;
	}
}
