package info.dragonlady.scriptlet;

import info.dragonlady.util.DBAccesser;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
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;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

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

	private static final long serialVersionUID = -1783518376805871958L;
	private static final String DEFAULT_CHARSET = "Shift-jis";
	private static final String DEFAULT_CONTENT_TYPE = "text/html";
	private String SCRIPTLET_PATH = "scriptlet_path";
	private String SITEMAP_PATH = "sitemap";
	private String DBCONFIG_PATH = "dbconfig";
	private String DBOBJECT_CONTROL = "dbobject";
	private String EXT_NAME = "extendName";
	private boolean sequenceControl = true;
	private boolean asynchronousInterface = false;
	protected static final String SEQUENCE_KEY = "info.dragonlady.scriptlet.SecureServlet#SEQUENCE_KEY";
	protected static final String SITEMAP_KEY = "info.dragonlady.scriptlet.SecureServlet#SITEMAP_KEY";
	protected int sequenceId = CORRECT_SEQUENCE;
	protected String charset = DEFAULT_CHARSET;
	protected String contentType = DEFAULT_CONTENT_TYPE;
	protected String scriptletPath = "WEB-INF"+File.separator+"scriptlet"+File.separator;
	protected String sitemapPath = "WEB-INF"+File.separator+"sitemap.xml";
	protected String dbConfigPath = "WEB-INF"+File.separator+"db_config.xml";
	protected Document sitemapXML = null;
	protected String defaultScriptClassName = null;
	protected DBAccesser dbaccesser = null;
	protected String extendName = null;
	private Properties properties = new Properties();
	public static final int CORRECT_SEQUENCE = 0;
	public static final int CONTEXT_ACCESS_SEQUENCE = 97;
	public static final int IGNORE_SEQUENCE = 98;
	public static final int INVALID_SEQUENCE = 99;
	public static final String WILD_CARD_SEQUENCE = "*";
	
	/**
	 * サイトマップXMLを解析します。
	 * @throws SystemErrorException
	 */
	protected void setupSiteMap(HttpSession session) throws SystemErrorException {
		try {
			String sitemapXMLPath = getRealPath() + sitemapPath;
			if(properties.getProperty(SITEMAP_PATH) != null && properties.getProperty(SITEMAP_PATH).length() > 2) {
				sitemapXMLPath = properties.getProperty(SITEMAP_PATH);
				File sitemapXMLFile = new File(sitemapXMLPath);
				if(sitemapXML != null && session.getAttribute(SITEMAP_KEY) != null) {
					if(Long.parseLong(session.getAttribute(SITEMAP_KEY).toString()) != sitemapXMLFile.lastModified()) {
						DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
						DocumentBuilder builder = factory.newDocumentBuilder();
						sitemapXML = builder.parse(sitemapXMLPath);
						session.setAttribute(SITEMAP_KEY, sitemapXMLFile.lastModified());
					}
				}else{
					DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
					DocumentBuilder builder = factory.newDocumentBuilder();
					sitemapXML = builder.parse(sitemapXMLPath);
					session.setAttribute(SITEMAP_KEY, sitemapXMLFile.lastModified());
				}
			}else{
				File sitemapXMLFile = new File(sitemapXMLPath);
				if(sitemapXML != null && session.getAttribute(SITEMAP_KEY) != null) {
					if(Long.parseLong(session.getAttribute(SITEMAP_KEY).toString()) != sitemapXMLFile.lastModified()) {
						DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
						DocumentBuilder builder = factory.newDocumentBuilder();
						sitemapXML = builder.parse(sitemapXMLPath);
						session.setAttribute(SITEMAP_KEY, sitemapXMLFile.lastModified());
					}
				}else{
					DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
					DocumentBuilder builder = factory.newDocumentBuilder();
					sitemapXML = builder.parse(sitemapXMLPath);
					session.setAttribute(SITEMAP_KEY, sitemapXMLFile.lastModified());
				}
			}
			if(sitemapXML == null) {
				throw new SystemErrorException("SITE-MAP XML NOT FOUND.");
			}else{
				NodeList siteNodes = sitemapXML.getElementsByTagName("site");
				if(siteNodes != null && siteNodes.getLength() > 0) {
					Element siteNode = (Element)siteNodes.item(0);
					if(siteNode.hasAttribute("class")) {
						defaultScriptClassName = siteNode.getAttribute("class");
						return;
					}
				}
				throw new SystemErrorException("'SITE' ATTRIBUTE or 'CLASS' ATTRIBUTE NOT FOUND IN SITE_MAP XML.");
			}
		}
		catch(SystemErrorException e) {
			throw e;
		}
		catch(Exception e) {
			throw new SystemErrorException(e);
		}
	}
	
	/**
	 * 
	 * @throws SystemErrorException
	 */
	protected void setupDBObject() throws SystemErrorException {
		try {
			if(useDBObject()) {
				String dbConfigXMLPath = getRealPath() + dbConfigPath;
				if(properties.getProperty(DBCONFIG_PATH) != null && properties.getProperty(DBCONFIG_PATH).length() > 2) {
					dbConfigXMLPath = properties.getProperty(DBCONFIG_PATH);
				}
				dbaccesser = new DBAccesser(new FileInputStream(dbConfigXMLPath));
			}
		}
		catch(Exception e) {
			throw new SystemErrorException(e);
		}
	}
	
	/**
	 * DBアクセスオブジェクトが利用可能か検証する。
	 * @return
	 */
	public boolean useDBObject() {
		if(properties.getProperty(DBOBJECT_CONTROL) != null && Boolean.parseBoolean(properties.getProperty(DBOBJECT_CONTROL))) {
			return true;
		}
		return false;
	}
	
	public DBAccesser getDBAccessObject() {
		return dbaccesser;
	}
	
	/**
	 * @throws IllegalAccessException
	 * @throws SystemErrorException 
	 */
	protected void initialize(HttpServletRequest req, HttpServletResponse res) throws IllegalAccessException, SystemErrorException{
		if(properties.getProperty("sequence_control") != null) {
			sequenceControl = Boolean.parseBoolean(properties.getProperty("sequence_control"));
		}
		if(properties.getProperty("force_content_length") != null &&
			properties.getProperty("force_content_length").toLowerCase().equals("true")) {
			ESEngine.forceContentLengthHeader = true;
		}
		extendName = properties.getProperty(EXT_NAME);
		if(extendName == null) {
			extendName = new String();
		}
		setContentType(DEFAULT_CONTENT_TYPE);
		setupSiteMap(req.getSession());
		setupDBObject();
		sequenceId = verifySequence(req);
		if(sequenceId == INVALID_SEQUENCE) {
			//シーケンス制御エラー時はシーケンスオブジェクトを削除
			req.getSession().setAttribute(SEQUENCE_KEY, null);
			throw new IllegalAccessException("Invalid sequence detected.");
		}
	}
	
	/**
	 * Scriptletを解決します。
	 * @return
	 * @throws SystemErrorException 
	 * @throws ClassNotFoundException 
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	protected Scriptlet buildScriptlet(HttpServletRequest req) throws SystemErrorException, ClassNotFoundException, InstantiationException, java.lang.IllegalAccessException {
		String myself = getRelativePath(req);
		NodeList pageNodes = sitemapXML.getElementsByTagName("page");
		for(int i=0;i<pageNodes.getLength();i++) {
			Element pageNode = (Element)pageNodes.item(i);
			if(pageNode.hasAttribute("path") && pageNode.getAttribute("path").equals(myself)) {
				if(pageNode.hasAttribute("class")) {
					String className = pageNode.getAttribute("class");
					Class<?> loadClass = this.getClass().getClassLoader().loadClass(className);
					Object scriptlet = (Object)loadClass.newInstance();
					if (scriptlet instanceof Scriptlet) {
						return (Scriptlet) scriptlet;
					}
				}
				Class<?> loadClass = this.getClass().getClassLoader().loadClass(defaultScriptClassName);
				Object scriptlet = (Object)loadClass.newInstance();
				if (scriptlet instanceof Scriptlet) {
					return (Scriptlet) scriptlet;
				}
			}
		}
		throw new SystemErrorException(String.format("Scriptlet not found(%s)", myself));
	}
	
	/**
	 * for Jetty Server
	 * @return
	 */
	protected String getRealPath() {
		String realPath = getServletContext().getRealPath("/");
		if(!realPath.endsWith("/")) {
			realPath += "/";
		}
		return realPath;
	}
	
	/**
	 * {@link HttpServlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
	 */
	protected void service(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException{
		try {
			String paramPath = getRealPath()+"WEB-INF"+File.separator+"config.xml";
			properties.loadFromXML(new FileInputStream(paramPath));
			initialize(req, res);
			if(sequenceId == IGNORE_SEQUENCE) {
				String ignoreFilePath = getScriptletPath() + getRelativePath(req);
				BufferedInputStream bis = new BufferedInputStream(new FileInputStream(ignoreFilePath));
				BufferedOutputStream bos = new BufferedOutputStream(res.getOutputStream());
				int readLen = 0;
				byte readBuffer[] = new byte[2048];
				while((readLen = bis.read(readBuffer)) > 0) {
					bos.write(readBuffer, 0, readLen);
					bos.flush();
				}
				bos.flush();
			}else
			if(sequenceId == CONTEXT_ACCESS_SEQUENCE) {
				res.sendRedirect(req.getContextPath()+"/");
			}else
			if(sequenceId == CORRECT_SEQUENCE) {
				Scriptlet scriptlet = buildScriptlet(req);
				scriptlet.setServlet(this, req, res);
				scriptlet.start();
				res.setContentType(getContentTypeValue());
			}else{
				res.sendError(404, String.format("NOT FOUND PATH(%s)", getRelativePath(req)));
			}
		}
		catch(SystemErrorException e) {
			log("ERROR:"+e.getMessage(),e);
			res.sendError(500, e.getMessage());
		}
		catch(IllegalAccessException e) {
			log("ERROR:"+e.getMessage(),e);
			res.sendError(403, e.getMessage());
		}
		catch(IOException e) {
			log("ERROR:"+e.getMessage(),e);
			res.sendError(404, e.getMessage());
		}
		catch(Exception e) {
			log("ERROR:"+e.getMessage(),e);
			res.sendError(404, e.getMessage());
		}
	}
	
	/**
	 * サイトマップXMLにてシーケンスの検証を行なう
	 * @param sessionSeqVal
	 * @param sequenceValue
	 * @return
	 */
	protected boolean checkSitemap(String sessionSeqVal, String sequenceValue) {
		NodeList pageNodes = sitemapXML.getElementsByTagName("page");
		for(int i=0;i<pageNodes.getLength();i++) {
			Element pageNode = (Element)pageNodes.item(i);
			if(pageNode.hasAttribute("path") && pageNode.getAttribute("path").equals(sequenceValue)) {
				Element parentNode = (Element)pageNode.getParentNode();
				//forward
				if(parentNode.hasAttribute("path") && parentNode.getAttribute("path").equals(sessionSeqVal)) {
					return true;
				}
				//reload or same process(ex page scroll)
				if(sequenceValue.equals(sessionSeqVal)) {
					return true;
				}
				//backward
				if(pageNode.hasAttribute("backward") && Boolean.parseBoolean(pageNode.getAttribute("backward"))){
					NodeList children = pageNode.getChildNodes();
					for(int j=0;j<children.getLength();j++) {
						if(children.item(j).getNodeType() == Node.ELEMENT_NODE) {
							Element child = (Element)children.item(j);
							if(child.hasAttribute("path") && child.getAttribute("path").equals(sessionSeqVal)) {
								return true;
							}
						}
					}
				}
			}
		}
		return false;
	}
	
	/**
	 * 正当なシーケンスで要求されているか検証します。
	 * デフォルト実装では、初期状態か処理系かの判別しか行いません。
	 * シーケンスを拡張する場合、オーバーライドして実装してください。
	 * @param servlet
	 * @return
	 * @throws IllegalAccessException 
	 */
	protected int verifySequence(HttpServletRequest req) throws IllegalAccessException {
		int result = sequenceControl ? INVALID_SEQUENCE : CORRECT_SEQUENCE;
		
		if(req.getContextPath().length() > 0 && req.getRequestURI().endsWith(req.getContextPath()) && !req.getRequestURI().endsWith("/")) { //
			return CONTEXT_ACCESS_SEQUENCE;
		}
		
		asynchronousInterface = false;
		if(sequenceControl) {
			Object sessionSeqVal = req.getSession().getAttribute(SEQUENCE_KEY);
			String sequenceValue = getInitSequence(req);
			if(sequenceValue != null && sequenceValue.equals(WILD_CARD_SEQUENCE)) {
				result = CORRECT_SEQUENCE;
			}else{
				if(sessionSeqVal != null){
					if(checkSitemap(sessionSeqVal.toString(), sequenceValue)){
						result = CORRECT_SEQUENCE;
					}else{
						if(!sequenceValue.endsWith(extendName)) {
							result = IGNORE_SEQUENCE;
						}
					}
				}else{
					throw new IllegalAccessException("Invalid sequece check process.");
				}
			}
		}
		
		return result;
	}
	
	/**
	 * 次のシーケンスに移行した際、正しいシーケンスであるためのフィンガープリントを設定します。
	 * デフォルトでは、クラス名＋serialVersionUIDをセッションに追加する。
	 * @param servlet
	 */
	protected void setSequence(HttpServletRequest req) {
		if(!asynchronousInterface) {
			req.getSession().setAttribute(SEQUENCE_KEY, getFingerprintForSequence(req));
		}
	}
	
	/**
	 * 現在設定されているシーケンスオブジェクトを取得する関数
	 * @return：シーケンスオブジェクト
	 */
	protected String getSequence(HttpSession session) {
		return session.getAttribute(SEQUENCE_KEY) == null ? null : session.getAttribute(SEQUENCE_KEY).toString();
	}
	
	/**
	 * 遷移画面でのシーケンス検証のため自分自身のフィンガープリントを作成する。
	 * @return
	 */
	protected String getFingerprintForSequence(HttpServletRequest req) {
		return getRelativePath(req);
	}
	
	/**
	 * 相対パスを取得する。
	 * @return
	 */
	protected String getRelativePath(HttpServletRequest req) {
		String path = req.getRequestURI();
		String contextPath = req.getContextPath();
		if(path.startsWith(contextPath)) {
			path = path.substring(contextPath.length());
		}
		if(path == null || path.length() < 2) {
			path = ESEngine.defaultScriptletName+getScriptExtName();
		}
		return path;
	}
	
	/**
	 * 初期シーケンス（Scriptlet）のverifySequence()にて、
	 * チェックする文字列を応答する。
	 * @return
	 * @throws IllegalAccessException 
	 */
	public String getInitSequence(HttpServletRequest req) throws IllegalAccessException {
		String myself = getRelativePath(req);
		NodeList pageNodes = sitemapXML.getElementsByTagName("page");
		for(int i=0;i<pageNodes.getLength();i++) {
			Element pageNode = (Element)pageNodes.item(i);
			if(pageNode.hasAttribute("path") && pageNode.getAttribute("path").equals(myself)) {
				if(pageNode.hasAttribute("asynchronous") && Boolean.parseBoolean(pageNode.getAttribute("asynchronous"))) {
					asynchronousInterface = true;
				}
				if(pageNode.hasAttribute("anyaccess") && Boolean.parseBoolean(pageNode.getAttribute("anyaccess"))) {
					return WILD_CARD_SEQUENCE;
				}else{
					return myself;
				}
			}
		}
		if(!myself.endsWith(extendName)) {
			return myself;
		}
		throw new IllegalAccessException("not found path on site-map("+myself+")");
	}

	/**
	 * HttpSessionクラスのインスタンスを応答します。
	 * @return：HttpSessionクラスのインスタンス
	 */
	public HttpSession getSession(HttpSession session) {
		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, HttpServletResponse res) {
		charset = code;
		res.setCharacterEncoding(charset);
	}
	
	/**
	 * スクリプトレットのパスを取得します。
	 * オーバーライドすることで、任意のパス構成を構築できます。
	 * 例）getScriptletPath(String any)でオーバーロードしてanyをsuper#getScriptletPathに付加する
	 * @return
	 */
	public String getScriptletPath() {
		String path = 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.substring(0, path.lastIndexOf(File.separator));
		}
		return path;
	}
	
	/**
	 * HttpServletResponse#setContentTypeに指定する、<br>
	 * ContentTypeの値を応答します。<br>
	 * デフォルトはtext/htmlです。変更したい場合はオーバーライド
	 * @return：コンテントタイプの文字列
	 */
	protected String getContentType() {
		return contentType;
	}

	/**
	 * コンテントタイプを変更します。（デフォルトはtext/html）
	 * @param type
	 */
	public void setContentType(String type) {
		contentType = type;
	}

	/**
	 * コンテンツの文字コードを取得します。
	 * @return
	 */
	private String getContentTypeValue() {
		return getContentType() + "; charset=" + getCharSet();
	}
	
	/**
	 * 設定ファイルに指定した、スクリプトレットの拡張子を応答する。
	 * @return
	 */
	public String getScriptExtName() {
		return extendName;
	}
	
	/**
	 * Servlet実行環境の応答
	 * @return
	 */
	public ServerEnvironment getServerEnvironment(){
		return ServerEnvironment.GOOGLE;
	}
}
