/*
 * $Id: ContextAccessor.java,v 1.12 2005/12/25 23:36:16 sugimotokenichi Exp $
 * 作成日: 2005/3/17
 */
package feat.v1;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import feat.v1.config.CommandDeclaration;
import feat.v1.config.FeatConfig;
import feat.v1.config.FeatureDeclaration;

/**
 * コンテキストへのアクセスを統一するためのクラス。
 * @author SUGIMOTO Ken-ichi
 */
public class ContextAccessor {

    // static -------------------------------------------------------

    protected static Log log = LogFactory.getLog(ContextAccessor.class);

    public static final int
    		SCOPE_PARAM = 1,
            SCOPE_REQUEST = 2,
            SCOPE_SESSION = 3,
            SCOPE_APPLICATION = 4;

    static FeatConfig getFeatConfig(ServletContext sctx) throws FeatException {
        FeatConfig conf = (FeatConfig)sctx.getAttribute(FeatConst.CONFIG_KEY);
        if ( conf == null )
            throw new FeatException("FeatConfigが取得できない", FeatException.LEVEL_FATAL);
        return conf;
    }

    static void setFeatConfig(ServletContext sctx, FeatConfig config) {
        sctx.setAttribute(FeatConst.CONFIG_KEY, config);
        sctx.setAttribute(FeatConst.CONFIG_LASTUPDATE_KEY, new Long(System.currentTimeMillis()));
    }

    static long getConfigLastUpdateTime(ServletContext sctx) {
        return ((Long)sctx.getAttribute(FeatConst.CONFIG_LASTUPDATE_KEY)).longValue();
    }


    // instance -----------------------------------------------------

    private ServletContext servletContext;
    private FeatConfig config;
    private HttpServletRequest request;
    private HttpServletResponse response;
    private FeatureDeclaration currentFeature;
    private CommandDeclaration currentCommand;
    private FeatureErrors errs;
    private Exception exception;

    /**
     *
     * @param servletContext ServletContext
     * @param request HttpServletRequest
     * @param response HttpServletResponse
     * @param featureName 現在のリクエストのフィーチャー名。リクエストURIから取り出した文字列。
     * @param commandName 現在のリクエストのコマンド名。リクエストURIから取り出した文字列。
     * @param createSession セッションが必要になったときに自動的に新規セッションを作成するならtrue。
     *         falseを指定するとHttpSessionが必要なメソッドを呼び出したときにHTTPSessionExceptionをスローする。
     * @throws FeatException FeatConfigオブジェクト、フィーチャー定義またはコマンド定義が見つからない
     */
    public ContextAccessor(ServletContext servletContext,
                           HttpServletRequest request,
                           HttpServletResponse response,
                           String featureName,
                           String commandName) throws FeatException {
        this.servletContext = servletContext;
        this.request = request;
        this.response = response;

        // パラメータをチェック
        // ここでインスタンス化後の例外の可能性を極力なくす
        config = getFeatConfig(servletContext);
        currentFeature = config.getFeatureDeclaration(featureName);
        if ( currentFeature == null )
            throw new FeatException("フィーチャーが取得できない:"+featureName, FeatException.LEVEL_WARN);
        currentCommand = currentFeature.getCommand(commandName);
        if ( currentCommand == null )
            throw new FeatException("コマンドが取得できない:"+commandName, FeatException.LEVEL_WARN);

        String enc = getEncoding();
        try {
            if ( enc != null )
                request.setCharacterEncoding(enc);
        }
        catch (UnsupportedEncodingException ex) {
            log.warn("サポートされていないエンコーディング:enc", ex);
        }
        errs = new FeatureErrors();
    }


    public HttpServletRequest getRequest() {
        return request;
    }

    public HttpServletResponse getResponse() {
        return response;
    }

    public ServletContext getServletContext() {
        return servletContext;
    }

    public FeatConfig getFeatConfig() {
        return config;
    }
    void setFeatConfig(FeatConfig config) {
        this.config = config;
        setFeatConfig(servletContext, config);
    }

    public FeatureDeclaration getCurrentFeatureDeclaration() {
        return currentFeature;
    }

    public CommandDeclaration getCurrentCommandDeclaration() {
        return currentCommand;
    }

    public HttpSession getSession() throws HTTPSessionException {
        HttpSession session = request.getSession(getCurrentFeatureDeclaration().isCreateSession());
        if ( session == null ) {
            throw new HTTPSessionException("HttpSessionが得られなかった");
        }
        return session;
    }

    /**
     * このリクエストの処理中に発生したエラーのリストを返す。
     * @return FeatureErrors
     */
    public FeatureErrors getFeatureErrors() {
        return errs;
    }

    /**
     * リクエストパラメータの名前を格納した配列を返す。
     * リクエストパラメータがない場合は空の配列を返す。
     * @return
     */
    public String[] getParameterNames() {
        Enumeration e = request.getParameterNames();
        ArrayList ret = new ArrayList();
        while ( e.hasMoreElements() ) {
            String name = (String)e.nextElement();
            ret.add(name);
        }
        if ( ret.size() == 0 )
            return new String[0];
        else
            return (String[])ret.toArray(new String[ret.size()]);
    }

    /**
     * リクエストパラメータの値を返す。
     * パラメータに複数の値が含まれるときは最初の一つを返す。
     * @param name
     * @return パラメータが無いときはnull
     * @see javax.servlet.ServletRequest#getParameter(java.lang.String)    */
    public String getParameter(String name) {
        return request.getParameter(name);
    }

    /**
     * リクエストパラメータの値を返す。
     * @param name
     * @return
     * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
     */
    public String[] getParameterValues(String name) {
        return request.getParameterValues(name);
    }

    /**
     * 指定パスのオブジェクトをMapインタフェースでラップして返す。
     * "param"を指定するとリクエストパラメータのマップ(値はString[]型)、
     * それ以外のときはオブジェクトのプロパティのマップを返す。
     * オブジェクトのパスは "スコープ名:属性名.プロパティのパス" の形式。
     * プロパティのパスは無くても良い。
     * @param path "param"またはgetAttribute(String)に渡すパス文字列(paramスコープ以外)
     * @return 指定パスのオブジェクトのプロパティマップ
     * @throws ContextAttributeAccessException paramスコープの後ろに属性名を指定した。
     *      	または存在しないスコープを指定した。
     * @throws HTTPSessionException セッションを開始できなかった
     */
    public Map getMappedValue(String path) throws ContextAttributeAccessException, HTTPSessionException {
        if ( path.startsWith("param") ) {
            if ( !path.equals("param") )
                throw new ContextAttributeAccessException("ここではparamスコープに属性名を指定できない - path:"+path, FeatException.LEVEL_ERROR);
            return request.getParameterMap();
        }
        else {
            return ObjectUtil.getBeanMap(getAttribute(path));
        }
    }

    /**
     * 属性を取得する。
     * スコープにSCOPE_PARAMを指定したときは単一の値のみ取得できる。
     * リクエストパラメータが複数の値を持つ場合は最初の一つだけを返す。
     * @param scope SCOPE_PARAM, SCOPE_REQUEST, SCOPE_SESSION, SCOPE_APPLICATIONの定数のどれか。
     * @param name 属性の名前
     * @return 属性の値。名前に対応した値がなければnullを返す
     * @throws HTTPSessionException セッションを開始できなかった
     * @throws ContextAttributeAccessException 存在しないスコープを指定した
     */
    public Object getAttribute(int scope, String name) throws
            HTTPSessionException, ContextAttributeAccessException {
        // アトリビュートのオブジェクトを取り出す
        Object o = null;
        switch(scope) {
            case SCOPE_PARAM:
                o = request.getParameter(name);
                break;
            case SCOPE_REQUEST:
                o = request.getAttribute(name);
                break;
            case SCOPE_SESSION:
                o = getSession().getAttribute(name);
                break;
            case SCOPE_APPLICATION:
                o = servletContext.getAttribute(name);
                break;
            default:
                throw new ContextAttributeAccessException("scope = "+scope, FeatException.LEVEL_ERROR);
        }
        return o;
    }

    /**
     * 属性を取得する。<br>
     * ネストした名前を指定できるので、オブジェクトツリーの中から任意のオブジェクトを取得することができる。<br>
     * 例えば <br>
     * ((Foo)session.getAttribute("user")).getName().getFirstName();<br>
     * は、このメソッドでは<br>
     * getAttribute(SCOPE_SESSION, "user", "name.firstName");<br>
     * と書くことができる。
     * @param scope SCOPE_PARAM, SCOPE_REQUEST, SCOPE_SESSION, SCOPE_APPLICATIONの定数のどれか。
     * @param name 属性の名前
     * @param path オブジェクトのプロパティ名。ネストした名前でも良い。
     * @throws HTTPSessionException セッションを開始できなかった
     * @throws ContextAttributeAccessException 存在しないスコープを指定した
     * @return 属性の値。名前に対応した値がなければnullを返す
     */
    public Object getAttribute(int scope, String name, String path) throws
            HTTPSessionException, ContextAttributeAccessException {

        // アトリビュートのオブジェクトを取り出す
        Object o = getAttribute(scope, name);

        if ( o != null ) {
            // ネストした名前だったらプロパティを取り出す
            try {
                o = ObjectUtil.getProperty(o, path);
            }
            catch (NoSuchMethodException e) {
                throw new ContextAttributeAccessException(e, FeatException.LEVEL_ERROR);
            }
            catch (InvocationTargetException e) {
                throw new ContextAttributeAccessException(e, FeatException.LEVEL_ERROR);
            }
            catch (IllegalAccessException e) {
                throw new ContextAttributeAccessException(e, FeatException.LEVEL_ERROR);
            }
        }

        return o;
    }

    /**
     * 属性を取得する。<br>
     * ネストした名前を指定できるので、オブジェクトツリーの中から任意のオブジェクトを取得することができる。<br>
     * 例えば <br>
     * ((Foo)session.getAttribute("user")).getName().getFirstName();<br>
     * は、このメソッドでは<br>
     * getAttribute("session:user.name.firstName");<br>
     * と書くことができる。
     * @param path "スコープ名:属性名"
     * @throws HTTPSessionException セッションを開始できなかった
     * @throws ContextAttributeAccessException 存在しないスコープを指定した
     * @return Object
     */
    public Object getAttribute(String path)
            throws ContextAttributeAccessException, HTTPSessionException {

        int colonIndex = path.indexOf(':');
        if ( colonIndex < 1 ) {
            throw new ContextAttributeAccessException("スコープが指定されていない", FeatException.LEVEL_ERROR);
        } else {
            // スコープ名を取り出す
            String scope = path.substring(0, colonIndex);

            // スコープ名を数値にする
            int intScope = -1;
            if ( scope.equalsIgnoreCase("param") )
                intScope = SCOPE_PARAM;
            else if ( scope.equalsIgnoreCase("request") )
                intScope = SCOPE_REQUEST;
            else if ( scope.equalsIgnoreCase("session") )
                intScope = SCOPE_SESSION;
            else if ( scope.equalsIgnoreCase("application") )
                intScope = SCOPE_APPLICATION;
            else
                throw new ContextAttributeAccessException("スコープ名が正しくない: "+scope, FeatException.LEVEL_ERROR);

            // 属性のパスを取り出して属性値を返す
            // 属性のパスに'.'が含まれていたら属性名とそれ以降のパスに分ける
            int dotIndex = path.indexOf('.', colonIndex);
            if ( dotIndex > -1 ) {
                String attrName = path.substring(colonIndex+1, dotIndex);
                String objectPath = path.substring(dotIndex+1);
                return getAttribute(intScope, attrName, objectPath);
            }
            else {
                String attrName = path.substring(colonIndex+1);
                return getAttribute(intScope, attrName);
            }
        }
    }

    /**
     * 属性をセットする。
     * @param scope SCOPE_REQUEST, SCOPE_SESSION, SCOPE_APPLICATIONの定数のどれか。
     * @param name 属性の名前
     * @param value 属性の値
     */
    public void setAttribute(int scope, String name, Object value) throws
            HTTPSessionException, ContextAttributeAccessException {

        switch(scope) {
            case SCOPE_REQUEST:
                request.setAttribute(name, value);
                break;
            case SCOPE_SESSION:
                getSession().setAttribute(name, value);
                break;
            case SCOPE_APPLICATION:
                servletContext.setAttribute(name, value);
                break;
            default:
                throw new ContextAttributeAccessException("scope = "+scope, FeatException.LEVEL_ERROR);
        }
    }

    /**
     * 現在のロケールのリソース文字列を返す。
     * @param name String
     * @return String
     */
    public String getStringResource(String name) {
        return getStringResource(getCurrentFeatureDeclaration(), name);
    }

    /**
     * 指定フィーチャーからリソース文字列を取得する。
     * ロケールはHTTPセッションに保存された言語設定、HTTPヘッダのAccept-Languageから得られたものを使用する。
     * @see #setLocale(Locale)
     * @see #getLocale()
     */
    public String getStringResource(FeatureDeclaration featureDecl, String name) {
        return getStringResource(featureDecl, name, getLocale());
    }

    /**
     * リソース文字列を取得する。
     */
    public String getStringResource(FeatureDeclaration featureDecl, String name, Locale locale) {
        // 指定のロケールでアクセスできる言語名を取得
        List sequence = Util.makeSequence(locale);

        // 「ロケール指定なし」を追加
        sequence.add(Util.NULL_LOCALE);

        // マッチするリソースを探す
        for(int i=0; i<sequence.size(); i++) {
            // フィーチャーのリソースを検索
            String ret = featureDecl.getStringResource(name, (Locale)sequence.get(i));
            // TODO FeatConfigのリソースを検索

            if ( ret != null )
                return ret;
        }
        return null;
    }

    /**
     * 現在のエンコーディング設定を返す。
     * エンコーディングはリクエストからパラメータを取得するときに参照される。
     * コマンド、フィーチャーの順で検索される。どちらにも定義されていないときはnull。
     * @return String
     */
    public String getEncoding() {
        String ret = null;

        ret = getCurrentCommandDeclaration().getEncoding();

        if (ret == null) {
            ret = getCurrentFeatureDeclaration().getEncoding();
        }

        return ret;
    }

    /**
     * このセッションのロケールを変更する。
     * @param locale
     */
    public void setLocale(Locale locale) throws HTTPSessionException {
        getSession().setAttribute(FeatConst.FEATURE_CONTEXT_LOCALE_KEY, locale);
    }

    /**
     * 現在のスコープのロケールを取得する。
     * ロケールの検索順は セッション -> HTTPヘッダ(Accept-Language) -> JVMのデフォルトロケール。
     * @return
     */
    public Locale getLocale() {
        Locale ret = null;
        try {
            ret = (Locale)getSession().getAttribute(FeatConst.FEATURE_CONTEXT_LOCALE_KEY);
        }
        catch (HTTPSessionException ex) {
        }

        FeatureDeclaration featureDec = getCurrentFeatureDeclaration();

        // ロケールを定義に含めるのはおかしい？
        // ロケールはユーザエージェントから指示されるものでアプリケーション側で定義するものではない。
        // フィーチャー定義やfeat-config.xmlのロケールはロケール情報が得られなかったときの
        // デフォルトロケールにすると良いかも。
        /*
        if ( ret == null && featureDec != null )
            ret = featureDec.getLocale();
        if ( ret == null )
            ret = context.getFeatConfig().getLocale();
         */

        // HTTPヘッダのロケールを解釈する
        if ( ret == null && request != null ) {
            String acceptLanguage = request.getHeader("Accept-Language");
            if ( acceptLanguage != null && acceptLanguage.length() > 0 ) {
                LanguageRange[] languageRange = parseLanguageRange(
                        acceptLanguage);
                if ( languageRange.length > 0 ) {
                    ret = languageRange[0].lang;
                }
            }
        }

        if ( ret == null )
            ret = Locale.getDefault();


        return ret;
    }

    private LanguageRange[] parseLanguageRange(String str) {
        String list[] = StringUtil.stripAll(StringUtil.split(str, ","));
        LanguageRange[] ret = new LanguageRange[list.length];
        for(int i=0; i<list.length; i++) {
            ret[i] = new LanguageRange(list[i], i);
        }
        Arrays.sort(ret);
        return ret;
    }


    private class LanguageRange implements Comparable {
        Locale lang;
        double quality;
        int index;

        LanguageRange(String str, int index) {
            this.index = index;

            if ( str == null || str.length() == 0 ) {
                lang = null;
                quality = 0.0D;
                return;
            }

            String[] part = StringUtil.stripAll(StringUtil.split(str, ";"));
            if ( part.length != 0 ) {
                lang = parseLang(part[0]);
                if ( part.length > 1 ) {
                    String[] token = StringUtil.stripAll(StringUtil.split(part[1], "="));
                    if ( token.length > 1 ) {
                        quality = parseQuality(token[1]);
                    }
                }
                else {
                    quality = 1.0D;
                }
            }
        }

        private Locale parseLang(String str) {
            Locale ret = null;
            if ( str != null && str.length() > 0 ) {
                String[] l = StringUtil.split(str, "-");
                if ( l.length == 1 ) {
                    ret = new Locale(l[0].toLowerCase());
                }
                else if ( l.length >= 2 ) {
                    ret = new Locale(l[0].toLowerCase(), l[1].toUpperCase());
                }
            }
            return ret;
        }

        private double parseQuality(String token) {
            try {
                return Double.parseDouble(token);
            }
            catch (NumberFormatException ex) {
                return 0.0D;
            }
        }

        public int compareTo(Object o) {
            return -compareTo_(o);
        }

        private int compareTo_(Object o) {
            LanguageRange oo = (LanguageRange)o;
            if ( quality < oo.quality )
                return -1;
            if ( quality > oo.quality )
                return 1;

            if ( index < oo.index )
                return -1;
            if ( index > oo.index )
                return 1;

            return 0;
        }
    }

    /**
     * リクエストの処理中に起きた例外をセットする。
     * @param ex
     */
    public void setException(Exception ex) {
        exception = ex;
    }

    /**
     * リクエストの処理中に起きた例外を返す。
     * @return
     */
    public Exception getException() {
        return exception;
    }

}
