package org.itscool.stylist.controller;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.itscool.commons.bean.BeanFactory;
import org.itscool.commons.collection.InitParamMap;
import org.itscool.commons.document.XmlConfigReader;
import org.itscool.commons.logging.AbstractLog;
import org.itscool.commons.logging.SimpleLog;
import org.itscool.commons.util.FileUtil;
import org.itscool.commons.util.TagInfo;
import org.itscool.commons.util.TagUtil;
import org.itscool.stylist.io.HtmlConfigReader;
import org.itscool.stylist.io.HtmlConverter;
import org.itscool.stylist.io.LayoutConfigReader;
import org.itscool.stylist.mapping.LayoutMapping;
import org.itscool.stylist.mapping.LayoutMappings;
import org.itscool.stylist.mapping.LayoutPutMapping;
import org.itscool.stylist.tag.HtmlRootTag;
import org.itscool.stylist.el.LayoutRegex;

/**
 * XHTMLを解析し、実行結果をXHTML形式で出力するクラスです<br/>
 * Stylist独自の構文を挿入することにより、動的な画面表示も可能です<br/>
 * またベースがXHTML形式であるため、AjaxやRSSとの連携もJSPよりシームレスに
 * 行うことができます。<br/>
 *
 * @author kano
 * @version 1.00A 2006/08/09 新規作成
 */
public class HtmlLayoutServlet extends HttpServlet{
    protected AbstractLog log;
    protected ServletContext context = null;
    protected HashMap cacheMap = new HashMap();
    
    /**
     * HtmlLayoutServletを初期化します<br>
     *
     * stylist-config.xmlからHTMLのレイアウト情報をロードします<br>
     * init()で例外が発生するとtomcatの起動に失敗するので、例外をキャッチ
     * した場合はスローせずにcatchして処理を正常に終了させている。
     * @param config FilterConfigインスタンス
     */
    public void init(ServletConfig config) throws ServletException {
        context = config.getServletContext();
        BeanFactory factory = BeanFactory.getInstance();
        if( !factory.isInit() ){
            try{
                factory.createForUrl("di-config.xml");
            }catch(Exception ex){
                ex.printStackTrace();
                throw new ServletException(ex.getMessage());
            }
        }
        try{
            log = (AbstractLog)factory.getInstance("Log");
            if(!log.isInit()){
                log = (AbstractLog)factory.createInstance("Log");
            }
        }catch(Exception ioe){
            log = SimpleLog.getInstance();
            log.info(ioe.getMessage());
        }
        LayoutMappings includeMappings = LayoutMappings.getInstance();
        
        try {
            InitParamMap params = (InitParamMap)factory.createInstance("InitParams");
            String stylistConf = params.getParam("stylist-config");
            if(stylistConf==null){
                String msg = "no define \"stylist-config\" path from diconfig.xml[InitParams]";
                log.info(msg);
            }else{
                XmlConfigReader includeReader = new LayoutConfigReader();
                includeReader.setConfigName(stylistConf);
                includeReader.create(includeMappings);
            }
        } catch (Exception ioe) {
            log.error(ioe);
        }
    }
    
    /**
     * doGetを実装します
     * @see javax.servlet.http.HttpServlet#doGet(
     *         javax.servlet.http.HttpServletRequest,
     *         javax.servlet.http.HttpServletResponse)
     */
    public void doGet(HttpServletRequest arg0, HttpServletResponse arg1)
    throws ServletException, IOException {
        execute(arg0, arg1);
    }
    
    /**
     * doPostを実装します
     * @see javax.servlet.http.HttpServlet#doPost(
     *         javax.servlet.http.HttpServletRequest,
     *         javax.servlet.http.HttpServletResponse)
     */
    public void doPost(HttpServletRequest arg0, HttpServletResponse arg1)
    throws ServletException, IOException {
        execute(arg0, arg1);
    }
    
    /**
     * doGet()、doPost()メソッドの処理を実行するためのexecuteメソッドを呼びます<br>
     * システム例外が発生した場合はexceptionメソッドを呼び出します。
     * @param req Httpリクエストインスタンス
     * @param res Httpレスポンスインスタンス
     */
    protected void execute(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException {
        try {
            process(req, res);
            
        } catch (IOException e) {
            String name1 = e.getClass().getName();
            String name2 = IOException.class.getName();
            exception(req, res, name1, name2, e);
            throw e;
        } catch (ServletException e) {
            String name1 = e.getClass().getName();
            String name2 = ServletException.class.getName();
            exception(req, res, name1, name2, e);
            throw e;
        } catch (NullPointerException e) {
            String name1 = e.getClass().getName();
            String name2 = NullPointerException.class.getName();
            exception(req, res, name1, name2, e);
            throw e;
        } catch (RuntimeException e) {
            String name1 = e.getClass().getName();
            String name2 = RuntimeException.class.getName();
            exception(req, res, name1, name2, e);
            throw e;
        } catch (Exception e) {
            String name1 = e.getClass().getName();
            String name2 = Exception.class.getName();
            exception(req, res, name1, name2, e);
            throw new ServletException(e.getMessage());
        } catch (Error e){
            String name1 = e.getClass().getName();
            String name2 = Exception.class.getName();
            exception(req, res, name1, name2, e);
            throw e;
        }
    }
    
    /**
     * doGet()、doPost()メソッドの処理を実行します<br>
     * @param req Httpリクエストインスタンス
     * @param res Httpレスポンスインスタンス
     */
    protected void process(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException {
        String path = context.getRealPath(req.getServletPath());
        BeanFactory factory = BeanFactory.getInstance();
        InitParamMap params = (InitParamMap)factory.getInstance("InitParams");
        
        String encode = params.getParam("encoding");
        if(encode == null){
            log.warn("No set inita param[encoding] default set[Shift_JIS]");
            encode = "Shift_JIS";
        }
        HtmlRootTag html = (HtmlRootTag)cacheMap.get(path);
        HtmlConfigReader reader = new HtmlConfigReader();
        reader.setDefaultEncode(encode);
        //キャッシュが存在しないあるいはキャッシュの更新日付が古い場合
        if( html == null || html.getLastModified()!=FileUtil.getLastModified(path)){
            html = new HtmlRootTag(encode);
            reader.create(path, html);
            cacheMap.put(path, html);
            log.trace("HtmlTag instance is created");
        }else{
            log.trace("HtmlTag instance geted from chache");
        }
        log.trace(html.toString(0, ""));
        String convStr;
        
        HtmlConverter converter = new HtmlConverter();
        converter.init(context, req);
        convStr = html.getXml()+System.getProperty("line.separator");
        convStr += html.getDtd()+System.getProperty("line.separator");
        convStr += converter.convert(html);
        //JSP1.2でEL式(簡易版)を使うための処理を追加
        convStr = new String(convStr);
        LayoutRegex regex = new LayoutRegex(context, req);
        //./で始まるパスをWebアプリケーションからの論理パスに変換
        convStr = regex.replacePath(convStr);
//delete 2007/03/19 kano start HtmlTextConverterにて処理
////add 2007/03/17 kano start
//        //Functionを展開する
//        String orgPkg = params.getParam("pkgName");
//        convStr = regex.replaceFunction(orgPkg, convStr);
////add 2007/03/17 kano end
//        //EL式を展開する
//        convStr = regex.replaceEl(convStr);
//delete 2007/03/19 kano end HtmlTextConverterにて処理

        //レイアウト情報を取得
        String servletPath = req.getServletPath();
        String layoutName = layout(servletPath, req);
        if( layoutName != null ){
            //HTML情報をHEAD部とBODY部に展開する
            TagInfo body = TagUtil.createTagInfo(convStr, TagInfo.BODY_NAME);
            TagInfo head = TagUtil.createTagInfo(convStr, TagInfo.HEAD_NAME);
            if( body != null ){
                //BODY内部の出力
                req.setAttribute(Globals.REQ_PARAM_INCLUDE_BODY,
                    body.getDataInTag());
                if(head != null ){
                    //HEAD要素記述部の出力（レイアウトで振り分ける）
                    req.setAttribute(Globals.REQ_PARAM_INCLUDE_HEAD,
                        head.getDataInTag());
                }
            }else{
                req.setAttribute(Globals.REQ_PARAM_INCLUDE_BODY, convStr);
            }

            //レイアウト情報を解析します
            String contextPath = req.getServerName();
            int port = req.getServerPort();
            String layoutPath = context.getRealPath(layoutName);
            HtmlRootTag layoutTag = new HtmlRootTag(encode);
            HtmlConfigReader layoutReader = new HtmlConfigReader();
            layoutReader.setDefaultEncode(encode);
            layoutReader.create(layoutPath, layoutTag);
            convStr = converter.convert(layoutTag);
            if( body != null ){
                //レイアウト要素のbody要素をメインコンテンツのbody要素に置き換える
                String bodyTagStr = body.getStartTag();
                Pattern pattern = Pattern.compile("<body>");
                Matcher matcher = pattern.matcher(convStr);
                convStr = matcher.replaceAll(body.getStartTag());
            }
        }

        if( !res.isCommitted() ){
            res.reset();
            //IE6でローカルHTMLのクッキーが保持されない現象への対応
            initSessionTimeout(req, res);
        }
        
        //ContentTypeを設定
        res.setContentType("text/html; charset=" + encode);
        
        //HTMLの解析結果を出力
        PrintWriter writer = res.getWriter();
        writer.write(convStr);
        writer.flush();
        writer.close();
    }
    
    /**
     * レイアウト情報を取得します
     * @param path クライアントから要求されたリクエストURI
     * @param req HttpServletRequestインスタンス
     * @return インクルードを含むフォワード先のJSP名
     */
    protected String layout(String path, HttpServletRequest req)
    throws IOException, ServletException{
        LayoutMappings mappings = LayoutMappings.getInstance();
        LayoutMapping include = (LayoutMapping) mappings.get(path);
        if (include == null) {
            return null;
        }
        
        String extendz = include.getExtendz();
        if (extendz != null) {
            //親のレイアウト要素を検査します
            checkParentLayout(include, req);
        }
        
        String layoutPath = include.getPath();
        HashMap putMap = include.getPuts();
        if (putMap != null) {
            
            //nameとpathをHttpServletRequestにセット
            //あとはJSP内のタグライブラリから<jsp:include page="${path}" />
            //を呼び出せばＯＫ！
            Set keySet = putMap.keySet();
            Iterator it = keySet.iterator();
            
            while (it.hasNext()) {
                String name = (String) it.next();
                LayoutPutMapping put = (LayoutPutMapping) putMap.get(name);
                setIncludeContents(put, name, req);
            }
        }
        
        return layoutPath;
    }
    
    /**
     * 親のレイアウト情報を取得します
     * @param layout レイアウト情報
     * @param req HttpServletRequestインスタンス
     */
    protected void checkParentLayout(
        LayoutMapping layout,
        HttpServletRequest req)
        throws IOException, ServletException{
        LayoutMappings mappings = LayoutMappings.getInstance();
        String parentName = layout.getExtendz();
        String extendz = layout.getExtendz();
        //更に親の要素がセットされていないかチェック
        if (extendz != null) {
            LayoutMapping parent = (LayoutMapping) mappings.get(parentName);
            checkParentLayout(parent, req);
        }
        
        HashMap putMap = layout.getPuts();
        if (putMap == null) {
            return;
        }
        
        //レイアウト用JSPにインクルードするJSPファイルを取得し、
        //HttpServletRequestインスタンスにセットする
        Set keySet = putMap.keySet();
        Iterator it = keySet.iterator();
        while (it.hasNext()) {
            String name = (String) it.next();
            
            LayoutPutMapping put = (LayoutPutMapping) putMap.get(name);
            setIncludeContents(put, name, req);
            
        }
    }
    
    /**
     * レイアウト情報から部分コンテンツ情報を取得し、リクエストパラメータ
     * にセットします
     * @param put レイアウト情報
     * @param attrName レイアウト情報をあらわすユニークな名前
     * @param req HttpServletRequest情報
     * @throws IOException
     */
    protected void setIncludeContents(LayoutPutMapping put,
        String attrName, HttpServletRequest req)
        throws IOException{
        String layoutName = put.getPath();
        
        //コンテンツ情報が格納されているパスを取得
        String contextPath = req.getServerName();
        int port = req.getServerPort();
        String layoutPath = context.getRealPath(layoutName);
        
        //コンテンツ情報をパースします
        BeanFactory factory = BeanFactory.getInstance();
        InitParamMap params = (InitParamMap)factory.getInstance("InitParams");
        String encode = params.getParam("encoding");
        if(encode == null){
            log.warn("No set inita param[encoding] default set[Shift_JIS]");
            encode = "Shift_JIS";
        }
        
        HtmlRootTag layoutTag = (HtmlRootTag)cacheMap.get(layoutPath);
        HtmlConfigReader reader = new HtmlConfigReader();
        reader.setDefaultEncode(encode);
        if( layoutTag == null || layoutTag.getLastModified()!=FileUtil.getLastModified(layoutPath)){
            layoutTag = new HtmlRootTag(encode);
            reader.create(layoutPath, layoutTag);
            cacheMap.put(layoutPath, layoutTag);
            log.trace("HtmlTag(layout) instance is created");
        }else{
            log.trace("HtmlTag(layout) instance geted from chache");
        }
        
        HtmlConverter converter = new HtmlConverter();
        converter.init(context, req);
        String convStr = converter.convert(layoutTag);
        
        TagInfo body = TagUtil.createTagInfo(convStr, TagInfo.BODY_NAME);
        if( body != null ){
            //BODY内部の出力
            req.setAttribute(Globals.REQ_PARAM_INCLUDE + attrName, body.getDataInTag());
        }else{
            req.setAttribute(Globals.REQ_PARAM_INCLUDE + attrName, convStr);
        }
    }
    
    /**
     * セッションの再設定
     * @param req HttpServletRequestインスタンス
     * @param res HttpServletResponseインスタンス
     */
    protected void initSessionTimeout(HttpServletRequest req, HttpServletResponse res){
        //IE6でローカルHTMLのクッキーが保持されない現象への対応
        HttpSession session = req.getSession();
        if( session.isNew()){
            String sessionId = session.getId();
            // クッキーの配列を取得
            Cookie cookies[] = req.getCookies();
            
            // 目的のクッキーを格納する変数
            Cookie accesstimeCookie = null;
            
            // それぞれのクッキーに対して名前を確認
            if(cookies != null) {
                for(int i = 0; i < cookies.length; i++) {
                    // 名前が "accesstime" であるかチェック
                    if(cookies[i].getName().equals("JSESSIONID")) {
                        // 該当するクッキーを取得
                        accesstimeCookie = cookies[i];
                    }
                }
            }
            
            //名前が"JSESSIONID"、値が現在時刻であるクッキーを作成
            Cookie cookie = new Cookie("JSESSIONID",sessionId);
            
            // クッキーの設定
            cookie.setMaxAge(7 * 24 * 60 * 60); //有効期間を1週間に設定
            
            // クッキーを発行
            res.addCookie(cookie);
        }
    }
    
    /**
     * 例外を出力します<BR
     * 例外は発生元のクラスでキャッチして呼出もとのクラスにthrowする
     * ことにより、例外のトレースを追跡する事ができます<BR>
     * 例外のトレースはJDK1.4で導入されたException.getStackTrace()
     * メソッドを使って例外をトレースしています
     * @param e Exceptionインスタンス
     * @return エラーページに遷移する場合はtrueを返します
     */
    protected void exception(
        HttpServletRequest req,
        HttpServletResponse res,
        String exceptionName1,
        String exceptionName2,
        Exception e){
        String msg = e.getMessage();
        if( msg == null ){
            msg =e.getClass().getName();
        }
        log.error(msg);
        StackTraceElement[] traceList = e.getStackTrace();
        for (int i = 0; i < traceList.length; i++) {
            StackTraceElement trace = traceList[i];
            log.error(trace.toString());
        }
        
    }
    
    /**
     * 例外を出力します<BR
     * 例外は発生元のクラスでキャッチして呼出もとのクラスにthrowする
     * ことにより、例外のトレースを追跡する事ができます<BR>
     * 例外のトレースはJDK1.4で導入されたException.getStackTrace()
     * メソッドを使って例外をトレースしています
     * @param req リクエスト
     * @param res レスポンス
     * @param exceptionName1 例外1
     * @param exceptionName2 例外2
     * @param e 例外情報
     */
    protected void exception(
        HttpServletRequest req,
        HttpServletResponse res,
        String exceptionName1,
        String exceptionName2,
        Error e) {
        String msg = e.getMessage();
        if( msg == null ){
            msg =e.getClass().getName();
        }
        log.error(msg);
        StackTraceElement[] traceList = e.getStackTrace();
        for (int i = 0; i < traceList.length; i++) {
            StackTraceElement trace = traceList[i];
            log.error(trace.toString());
        }
    }
}


