package info.dragonlady.scriptlet;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * 処理シーケンスの妥当性検証を実装した、javax.servlet.http.HttpServletの継承クラスです。
 * Scriptletクラスの基底クラスで、このクラスを直接継承することはありません。
 * @author nobu
 *
 */
abstract public class SecureServlet extends HttpServlet {

	private static final long serialVersionUID = -1783518376805871958L;
	private static final String DEFAULT_CHARSET = "Shift-jis";
	private static final String DEFAULT_CONTENT_TYPE = "text/html";
	private HttpSession session = null;
	private HttpServletRequest request = null;
	private HttpServletResponse response = null;
	private String SCRIPTLET_PATH = "scriptlet_path";
	protected static final String SEQUENCE_KEY = "info.dragonlady.scriptlet.SecureServlet#SEQUENCE_KEY";
	protected int sequenceId = INIT_SEQUENCE;
	protected String charset = DEFAULT_CHARSET;
	protected String scriptletPath = "WEB-INF"+File.separator+"scriptlet"+File.separator;
	private Properties properties = new Properties();
	public static final int INIT_SEQUENCE = 0;
	public static final int EXEC_SEQUENCE = 1;
	public static final int INVALID_SEQUENCE = 99;
	
	/**
	 * serialVersionUIDを応答する仮想関数
	 * @return
	 */
	abstract public long getSerialVersionUID();

	/**
	 * @throws IllegalAccessException
	 */
	protected void initialize() throws IllegalAccessException{
		sequenceId = verifySequence();
		if(sequenceId == INVALID_SEQUENCE) {
			//TODO
			//シーケンス制御エラー時はシーケンスオブジェクトを削除
			session.setAttribute(SEQUENCE_KEY, null);
			throw new IllegalAccessException("Invalid sequence detected.");
		}
	}
	
	/**
	 * {@link HttpServlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
	 */
	protected final void service(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException{
		try {
			session = req.getSession();
			request = req;
			response = res;
			String paramPath = getServletContext().getRealPath("/")+"WEB-INF"+File.separator+"config.xml";
			properties.loadFromXML(new FileInputStream(paramPath));
			initialize();
			super.service(req, res);
			res.setContentType(getContentTypeValue());
		}
		catch(IllegalAccessException e) {
			//TODO
			e.printStackTrace(System.out);
			res.sendError(403, e.getMessage());
		}
		catch(IOException e) {
			//TODO
			e.printStackTrace(System.out);
			res.sendError(404, e.getMessage());
		}
		catch(ServletException e) {
			//TODO
			e.printStackTrace(System.out);
			res.sendError(404, e.getMessage());
		}
		catch(Exception e) {
			//TODO
			e.printStackTrace(System.out);
			res.sendError(404, e.getMessage());
		}
	}
	
	/**
	 * 正当なシーケンスで要求されているか検証します。
	 * デフォルト実装では、初期状態か処理系かの判別しか行いません。
	 * シーケンスを拡張する場合、オーバーライドして実装してください。
	 * @param servlet
	 * @return
	 */
	protected int verifySequence() {
		int result = INIT_SEQUENCE;
		Object sessionSeqVal = session.getAttribute(SEQUENCE_KEY);
		String pathName = request.getRequestURL().toString();
		pathName = pathName.startsWith("/") ? pathName.substring(1) : pathName;
		String svu = Long.toString(getSerialVersionUID());
		if(sessionSeqVal != null){
			if(sessionSeqVal.equals(getInitSequence())){
				result = INIT_SEQUENCE;
			}else
			if(sessionSeqVal.equals(pathName+svu)) {
				if(isExecSequence()) {
					result = EXEC_SEQUENCE;
				}else{
					result = INIT_SEQUENCE;
				}
			}else{
				result = getDefaultSequence();
			}
		}else{
			if(getInitSequence() != sessionSeqVal) {
				result = INVALID_SEQUENCE;
			}
		}
		
		return result;
	}
	
	/**
	 * 処理シーケンスか初期状態か検証します。
	 * デフォルト実装では、要求パラメータの有無で検証します。
	 * 拡張する場合は、オーバーライド実装してください。
	 * @return
	 */
	protected boolean isExecSequence() {
		return !request.getParameterMap().isEmpty();
	}
	
	/**
	 * 次のシーケンスに移行した際、正しいシーケンスであるためのフィンガープリントを設定します。
	 * デフォルトでは、クラス名＋serialVersionUIDをセッションに追加する。
	 * @param servlet
	 */
	protected void setSequence() {
		String pathName = request.getRequestURL().toString();
		pathName = pathName.startsWith("/") ? pathName.substring(1) : pathName;
		String svu = Long.toString(getSerialVersionUID());
		if(isExecute()) {
			session.setAttribute(SEQUENCE_KEY, getNextInitSequence());
		}else{
			session.setAttribute(SEQUENCE_KEY, pathName+svu);
		}
	}
	
	/**
	 * 現在設定されているシーケンスオブジェクトを取得する関数
	 * @return：シーケンスオブジェクト
	 */
	protected String getSequence() {
		return session.getAttribute(SEQUENCE_KEY) == null ? null : session.getAttribute(SEQUENCE_KEY).toString();
	}
	
	/**
	 * シーケンス条件が適合しない場合の、動作設定を応答します。
	 * デフォルトは初期状態を応答。
	 * 注.直リンクを許可しないサーブレットは"INVALID_SEQUENCE"を応答するよう、オーバーライドする必要があります。
	 * @return
	 */
	protected int getDefaultSequence() {
		return INIT_SEQUENCE;
	}

	/**
	 * 初期モードか、実行モードかを応答する。
	 * @return
	 */
	public boolean isExecute() {
		if(sequenceId == EXEC_SEQUENCE) {
			return true;
		}
		return false;
	}

	/**
	 * 次の初期シーケンス（Scriptlet）のverifySequence()にて、
	 * チェックする文字列を返す仮想関数
	 * @return
	 */
	abstract public String getNextInitSequence();
	/**
	 * 初期シーケンス（Scriptlet）のverifySequence()にて、
	 * チェックする文字列を返す仮想関数
	 * @return
	 */
	abstract public String getInitSequence();

	/**
	 * HttpSessionクラスのインスタンスを応答します。
	 * @return：HttpSessionクラスのインスタンス
	 */
	public HttpSession getSession() {
		return session;
	}
	
	/**
	 * HttpServletRequestクラスのインスタンスを応答します。
	 * @return：HttpServletRequestクラスのインスタンス
	 */
	public HttpServletRequest getRequest() {
		return request;
	}
	
	/**
	 * HttpServletResponseクラスのインスタンスを応答します。
	 * @return：HttpServletResponseクラスのインスタンス
	 */
	public HttpServletResponse getResponse() {
		return response;
	}
	
	/**
	 * HttpServletResponse#setContentTypeに指定する、<br>
	 * CharSetの値を応答します。<br>
	 * デフォルトはShift-jisです。変更したい場合はオーバーライド
	 * @return：文字コードの文字列（IANA）
	 */
	protected String getCharSet() {
		return charset;
	}
	
	/**
	 * 文字コードを変更します。
	 * @param code
	 */
	public void setCharSet(String code) {
		charset = code;
		response.setCharacterEncoding(charset);
	}
	
	/**
	 * スクリプトレットのパスを取得します。
	 * オーバーライドすることで、任意のパス構成を構築できます。
	 * 例）getScriptletPath(String any)でオーバーロードしてanyをsuper#getScriptletPathに付加する
	 * @return
	 */
	public String getScriptletPath() {
		String path = getServletContext().getRealPath("/") + scriptletPath;
		
		if(properties.getProperty(SCRIPTLET_PATH) != null && properties.getProperty(SCRIPTLET_PATH).length() > 2) {
			path = properties.getProperty(SCRIPTLET_PATH);
			if(!path.endsWith(File.separator)) {
				path = path + File.separator;
			}
		}
		return path;
	}
	
	/**
	 * HttpServletResponse#setContentTypeに指定する、<br>
	 * ContentTypeの値を応答します。<br>
	 * デフォルトはtext/htmlです。変更したい場合はオーバーライド
	 * @return：コンテントタイプの文字列
	 */
	protected String getContentType() {
		return DEFAULT_CONTENT_TYPE;
	}
	
	/**
	 * コンテンツの文字コードを取得します。
	 * @return
	 */
	private String getContentTypeValue() {
		return getContentType() + "; charset=" + getCharSet();
	}

}
