/*
 * Copyright 2006 Takahiro Nakamura.
 *
 * 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 woolpack.ee;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;

import org.w3c.dom.Element;

import woolpack.dom.DomConstants;
import woolpack.dom.DomContext;
import woolpack.dom.DomExpression;
import woolpack.dom.Serial;
import woolpack.dom.XPath;
import woolpack.utils.UtilsConstants;
import woolpack.utils.XPathWrapper;

/**
 * 画面遷移順序保証のためのトランザクショントークンを検証・登録しDOMノードへ設定するする{@link DomExpression}の生成器。
 * 関数従属性を考慮すると画面遷移順序を保証する id を画面遷移定義に記載するデータモデルが妥当であるが、
 * 画面遷移順序を保証する id の一覧を独立したエンティティとして扱うデータモデルが設計上保守性が維持できると判断し、
 * 本実装では画面遷移順序を保証する id の一覧を、画面遷移定義と別に{@link Collection}で定義している。
 * @author nakamura
 *
 */
public class TransactionBuilder {
	
	private static final XPathWrapper XPATH_FORM = new XPathWrapper("//FORM");
	private static final XPathWrapper XPATH_HREF = new XPathWrapper("//A[@href]");

	/**
	 * トランザクショントークンを格納する{@link DomContext#getSession()}上の位置のデフォルト値。
	 */
	public static final String TRANSACTION_TOKEN_KEY = "woolpack.transaction.TRANSACTION_TOKEN";
	
	private final String key;
	private final Random r;
	private final Collection<String> idCollection;
	private final int size;

	/**
	 * コンストラクタ。
	 * @param key トークンを格納する{@link DomContext#getSession()}の位置。
	 * @param idCollection トランザクションを検証する id の一覧。
	 * @param size トークンのサイズ。
	 * @throws StringIndexOutOfBoundsException key が空の場合。
	 * @throws NullPointerException 引数が null の場合。
	 */
	public TransactionBuilder(final String key, final Collection<String> idCollection, final int size){
		key.charAt(0);
		idCollection.getClass();
		
		this.key = key;
		this.r = new Random();
		this.idCollection = idCollection;
		this.size = size;
	}
	
	/**
	 * コンストラクタ。
	 * トークンを格納する{@link DomContext#getSession()}の位置を{@link TransactionBuilder#TRANSACTION_TOKEN_KEY}とする。
	 * トークンのサイズを32とする。
	 * @param idCollection トランザクションを検証する id の一覧。
	 */
	public TransactionBuilder(final Collection<String> idCollection){
		this(TRANSACTION_TOKEN_KEY, idCollection, 32);
	}

	/**
	 * トークンを格納する{@link DomContext#getSession()}の位置を返す。
	 * @return トークンを格納する{@link DomContext#getSession()}の位置。
	 */
	public String getKey() {
		return key;
	}
	
	/**
	 * トークンを検証しトークンを生成して{@link DomContext#getSession()}に格納する{@link DomExpression}を返す。
	 * @param trueExpression 妥当なトランザクションの場合の委譲先。
	 * @param falseExpression 妥当でないトランザクションの場合の委譲先。
	 * @return トークンを検証する{@link DomExpression}。
	 * @throws NullPointerException 引数のいずれかが null の場合。
	 */
	public DomExpression getCheckExpression(final DomExpression trueExpression, final DomExpression falseExpression){
		trueExpression.getClass();
		falseExpression.getClass();
		return new DomExpression(){
			public synchronized void interpret(final DomContext context) {
				// TODO セッションごとの同期化
				try{
					if(idCollection.contains(context.getId())){
						final Object sessionToken = context.getSession().get(key);
						final List requestedTokens = UtilsConstants.toList(context.getInput().get(key));
						
						if(sessionToken  == null || !sessionToken.equals(requestedTokens.get(0))){
							falseExpression.interpret(context);
							return;
						}
					}
					trueExpression.interpret(context);
				}finally{
					final StringBuilder sb = new StringBuilder();
					for(int i=0; i<size; i++){
						// jは正の数
						int j = r.nextInt(36);
						sb.append( (j<10)?((char)('0' + j)):((char)('A'+j-10)) );
					}
					context.getSession().put(key, sb.toString());
				}
			}
		};
	}
	
	/**
	 * トークンを DOM ノードに設定する{@link DomExpression}を返す。
	 * HTML フォームを検索し子ノードとして hidden エレメントを追加する。
	 * そしてアンカーを検索し HTML にパラメータを追加する。
	 * {@link DomExpression#interpret(DomContext)}は引数または{@link DomContext#getNode()}または{@link DomContext#getSession()}が null の場合に{@link NullPointerException}を投げる。
	 * @return トークンを DOM ノードに設定する{@link DomExpression}。
	 */
	public DomExpression getAppendExpression(){
		return new Serial(
				new XPath(XPATH_FORM, new DomExpression(){
					public void interpret(final DomContext context) {
						final String token = (String)context.getSession().get(key);
						if(token != null){
							final Element element = DomConstants.getDocumentNode(context.getNode()).createElement("INPUT");
							element.setAttribute("type", "hidden");
							element.setAttribute("name", key);
							element.setAttribute("value", token);
							context.getNode().appendChild(element);
						}
					}
				}),
				new XPath(XPATH_HREF, new DomExpression(){
					public void interpret(final DomContext context) {
						final String token = (String)context.getSession().get(key);
						if(token != null){
							final Element element = (Element)context.getNode();
							final String href = element.getAttribute("href");
							final String newHref = href + ((href.indexOf('?') >= 0)?'&':'?') + key + "=" + token;
							element.setAttribute("href", newHref);
						}
					}
				})
		);
	}
	
	/**
	 *  トランザクションを検証する id の一覧を返す。
	 * @return トランザクションを検証する id の一覧。
	 */
	public Collection<String> getIdCollection(){
		return Collections.unmodifiableCollection(idCollection);
	}
}
