/*
 * $Id: CommandContext.java,v 1.10 2006/05/11 15:36:08 sugimotokenichi Exp $
 * Copyright (C) 2005 SUGIMOTO Ken-ichi
 * 作成日: 2006/03/01
 */
package feat2;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
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.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import feat2.config.CommandConfig;
import feat2.config.ConfigurationException;
import feat2.config.FeatConfig;
import feat2.config.FeatureConfig;
import feat2.config.type.Scope;
import feat2.impl.Cache;

/**
 * コマンド実行のコンテクスト。ライフサイクルはリクエストとほぼ同じだが、
 * リクエストがフォワードされたときはフォワード先で新しいコンテクかストが作られる。
 * @author SUGIMOTO Ken-ichi
 */
public class CommandContext implements ResourceManager {

    //private static Log log = LogFactory.getLog(ActionContext.class);
    private static Cache localCache;

    static {
        localCache = new Cache();
    }

    private FeatConfig config;
    private ServletContext servletContext;
    private HttpServletRequest request;
    private HttpServletResponse response;
    private FeatErrors featureErrors;
    private HashMap attributes;
    private String featureName;
    private String commandName;
    private String outputName;
    private Throwable exception;
    private HashMap multipartItems;


    public CommandContext(FeatConfig config, ServletContext servletContext,
            HttpServletRequest request,
            HttpServletResponse response) throws HttpNotFoundException {

        this.config = config;
        this.servletContext = servletContext;
        this.request = request;
        this.response = response;
        featureErrors = new FeatErrors(this);
        attributes = new HashMap();

        // リクエストURIからFeature、コマンド名を取り出す
        String servletPath = request.getServletPath();
        if ( servletPath != null ) {
            String[] pathInfo = StringUtil.split(servletPath, "/");
            if ( pathInfo.length > 0 )
                featureName = pathInfo[0];
            if ( pathInfo.length > 1 )
                commandName = StringUtil.substringBefore(pathInfo[1], ".");
        }
        if ( featureName == null ) {
            throw new HttpNotFoundException("フィーチャー名が指定されていません"); // TODO リソースに置き換え
        }
        if ( commandName == null ) {
            throw new HttpNotFoundException("コマンド名が指定されていません"); // TODO リソースに置き換え
        }

        // リクエストパラメータのエンコーディング

        String encoding = getEncoding();
        if ( encoding != null )
            try {
                request.setCharacterEncoding(encoding);
            }
            catch (UnsupportedEncodingException ex) {
                throw new ConfigurationException(ex);
            }
    }

    public HttpServletRequest getRequest() {
        return request;
    }

    /**
     * multipart/form-dataのリクエスト本文を解釈して返す。
     * このメソッドはアプリケーションがHttpServletRequestのgetParameterメソッドや
     * getInputStreamメソッド、getReaderメソッドを呼び出したあとでは動作しない。
     * @return リクエスト本文のマップMap&lt;String, MultipartFormItem&gt;。リクエスト本文がmultipart/form-dataでないときはnullを返す。
     * @throws feat2.FileUploadException リクエスト本文のパース中に問題が起きたとき
     */
    public Map getMultipartRequestBody() throws feat2.FileUploadException {

        if ( multipartItems != null )
            return multipartItems;

        if ( !isMultipartRequest() )
            return null;

        multipartItems = new HashMap();

        FileItemFactory factory = new DiskFileItemFactory();
        ServletFileUpload upload = new ServletFileUpload(factory);
        List items;
        try {
            items = upload.parseRequest(request);
        }
        catch (FileUploadException ex) {
            throw new feat2.FileUploadException(ex);
        }

        Iterator iter = items.iterator();
        while (iter.hasNext()) {
            FileItem item = (FileItem) iter.next();

            MultipartFormItem formItem = new MultipartFormItem(item);

            multipartItems.put(formItem.getFieldName(), formItem);

        }

        return multipartItems;

    }


    /**
     * HTTPリクエストがマルチパートリクエストかどうかを返す。
     */
    public boolean isMultipartRequest() {

        return multipartItems != null ||
            ( request.getMethod().equalsIgnoreCase("POST") &&
              request.getContentType().indexOf("multipart/form-data") > -1 );

    }


    public class MultipartFormItem {

        private FileItem item;

        private MultipartFormItem(FileItem item) {
            this.item = item;
        }

        public String getContentType() {
            return item.getContentType();
        }

        public String getFieldName() {
            return item.getFieldName();
        }

        public String getFileName() {
            return item.getName();
        }

        public long getSize() {
            return item.getSize();
        }

        /**
         * フィールドの内容を返す。
         * エンコーディングはcommandまたはfeatureのencodingで設定された値か、デフォルトエンコーディング。
         * @return
         */
        public String getString() {
            String encoding = getEncoding();
            if ( encoding == null )
                encoding = System.getProperty("file.encoding");
            try {
                return item.getString(encoding);
            }
            catch (UnsupportedEncodingException ex) {
                throw new FeatRuntimeException(""); // TODO エラーメッセージ
            }
        }

        public boolean isFormField() {
            return item.isFormField();
        }

        public boolean isInMemory() {
            return item.isInMemory();
        }

        public byte[] get() {
            return item.get();
        }

        public File getFile() throws IOException {

            File ret = File.createTempFile("feat_", null);

            try {
                item.write(ret);
            }
            catch (Exception ex) {
                throw new FeatRuntimeException(ex);
            }

            return ret;

        }

    }


    public HttpServletResponse getResponse() {
        return response;
    }

    public ServletContext getServletContext() {
        return servletContext;
    }


    /**
     * コマンドコンテクストからオブジェクトを取り出す。
     * @param name
     * @return
     */
    public Object getAttribute(String name) {
        return attributes.get(name);
    }
    /**
     * コマンドコンテクストにオブジェクトを追加する。
     * @param name
     * @param o
     */
    public void setAttribute(String name, Object o) {
        attributes.put(name, o);
    }


    public Object getCacheObject(String name) {
        return localCache.get(name);
    }
    public void putCacheObject(String name, Object obj, long ttl) {
        localCache.put(name, obj, ttl);
    }


    public Object getAttribute(String name, Scope scope) {

        Object ret = null;

        switch (scope.getScope() ) {

            case Scope.LOCAL:
                ret = getAttribute(name);
                break;

            case Scope.REQUEST:
                ret = request.getAttribute(name);
                break;

            case Scope.USER:
                // TODO USERコンテクストからのオブジェクト参照
                break;

            case Scope.SESSION:
                try {
                    ret = getSession().getAttribute(name);
                }
                catch (NullPointerException ex) {
                }
                break;

            case Scope.APPLICATION:
                ret = servletContext.getAttribute(name);
                break;

        }

        return ret;

    }

    public void setAttribute(String name, Object value, Scope scope) throws HTTPSessionException {

        switch (scope.getScope() ) {

            case Scope.LOCAL:
                setAttribute(name, value);
                break;

            case Scope.REQUEST:
                request.setAttribute(name, value);
                break;

            case Scope.USER:
                // TODO USERコンテクストにオブジェクトをセット
                break;

            case Scope.SESSION:
                HttpSession session = getSession();
                if ( session == null )
                    throw new HTTPSessionException("Beanをセットしようとしましたがセッションが得られませんでした "+getCurrentCommandConfig());
                session.setAttribute(name, value);
                break;

            case Scope.APPLICATION:
                servletContext.setAttribute(name, value);
                break;

        }

    }


    /**
     * エンコーディングを修正したリクエストパラメータを返す。
     */
    public String getRequestParameter(String name) {

        String ret = request.getParameter(name);
        String encoding = getEncoding();

        if ( ret != null
                && request.getMethod().equalsIgnoreCase("get")
                && config.isUseBodyEncodingForURI()
                && encoding != null ) {

            try {
                ret = StringUtil.convertEncoding(ret, encoding);
                ret = StringUtil.correctJapaneseChars(ret, encoding);
            }
            catch (UnsupportedEncodingException ex) {
                throw new ConfigurationException(ex);
            }

        }

        return ret;

    }


    /**
     * エンコーディングを修正したリクエストパラメータを返す。
     */
    public String[] getRequestParameterValues(String name) {

        String[] ret = request.getParameterValues(name);
        String encoding = getEncoding();

        if ( ret != null
                && request.getMethod().equalsIgnoreCase("get")
                && config.isUseBodyEncodingForURI()
                && encoding != null ) {

            try {
                ret = StringUtil.convertEncoding(ret, encoding);
                for (int i = 0; i < ret.length; i++) {
                    ret[i] = StringUtil.correctJapaneseChars(ret[i], encoding);
                }
            }
            catch (UnsupportedEncodingException ex) {
                throw new ConfigurationException(ex);
            }

        }

        return ret;

    }


    /**
     * エンコーディングを修正したリクエストパラメータを返す。
     * @return Map&lt;String, String[]&gt;
     */
    public Map getRequestParameterMap() {

        Map params = request.getParameterMap();
        HashMap ret = new HashMap(params.size());

        String encoding = getEncoding();

        if ( request.getMethod().equalsIgnoreCase("get")
                && config.isUseBodyEncodingForURI()
                && encoding != null ) {

            try {

                for(Iterator it=params.keySet().iterator(); it.hasNext(); ) {

                    String key = (String)it.next();
                    String[] value = (String[])params.get(key);

                    value = StringUtil.convertEncoding(value, encoding);

                    for (int i = 0; i < value.length; i++) {
                        value[i] = StringUtil.correctJapaneseChars(value[i], encoding);
                    }

                    ret.put(key, value);

                }

            }
            catch (UnsupportedEncodingException ex) {
                throw new ConfigurationException(ex);
            }

        }

        return ret;

    }




    /**
     * 設定内容を返す。
     */
    public FeatConfig getFeatConfig() {
        return config;
    }

    /**
     * セッションを返す。
     * @return セッションがまだ生成されておらず、セッション生成フラグがfalseだったらnull
     */
    public HttpSession getSession() {
        Boolean createSession = getCurrentCommandConfig().getCreateSession();
        if ( createSession == null )
            createSession = getCurrentFeatureConfig().getCreateSession();

        HttpSession session = request.getSession(createSession != null && createSession.booleanValue());
        return session;
    }

    /**
     * 現在のリクエストで指定されたフィーチャーの設定を返す。
     * @return 見つからなかったらnull
     */
    public FeatureConfig getCurrentFeatureConfig() {
        try {
            return getFeatConfig().getFeatureConfig(featureName);
        }
        catch(NullPointerException ex) {
            return null;
        }
    }

    /**
     * 現在のリクエストで指定されたコマンドの設定を返す。
     * @return 見つからなかったらnull
     */
    public CommandConfig getCurrentCommandConfig() {
        try {
            return getCurrentFeatureConfig().getCommandConfig(commandName);
        }
        catch (RuntimeException ex) {
            return null;
        }
    }

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

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

        ret = getCurrentCommandConfig().getEncoding();

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

        return ret;
    }

    public String getOutputName() {
        return outputName;
    }
    /**
     * コマンドのアウトプット名を設定する。
     * @return
     */
    public void setOutputName(String outputName) {
        this.outputName = outputName;
    }

    /**
     * 処理中に起こった例外を返す。
     * @return
     */
    public Throwable getException() {
        return exception;
    }

    public void setException(Throwable exception) {
        this.exception = exception;
    }

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

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

    /**
     * リソース文字列を取得する。
     */
    public String getStringResource(FeatureConfig featureConf, String name, Locale[] locale) {

        for(int i=0;i <locale.length; i++) {
            String ret = getStringResource(featureConf, name, locale[i]);
            if ( ret != null )
                return ret;
        }

        // ロケール指定なし
        return getStringResource(featureConf, name, Util.NULL_LOCALE);
    }

    /**
     * 指定のロケールから類推されるロケールでリソースを探す。
     * @param featureConf
     * @param resourceName
     * @param sequence
     * @return
     */
    private String getStringResource(FeatureConfig featureConf, String resourceName, Locale locale) {
        String ret = null;

        // ロケールを類推する
        List sequence = null;
        if ( locale != Util.NULL_LOCALE )
            sequence = Util.expandLocale(locale);
        else {
            sequence = new ArrayList();
            sequence.add(Util.NULL_LOCALE);
        }

        // フィーチャーのリソースを検索
        for(int i=0; i<sequence.size(); i++) {
            ret = featureConf.getStringResource(resourceName, (Locale)sequence.get(i));
            if ( ret != null )
                return ret;
        }

        // feat設定ファイルのリソースを検索
        for(int i=0; i<sequence.size(); i++) {
            ret = featureConf.getFeatConfig().getStringResource(resourceName, (Locale)sequence.get(i));
            if ( ret != null )
                return ret;
        }

        return null;
    }

    /**
     * このセッションのロケールを変更する。
     * ここで設定したロケールはセッションが維持される限り保持される。
     * @param locale
     */
    public void setSessionLocale(Locale locale) {
        try {
            getSession().setAttribute(FeatConst.FEATURE_CONTEXT_LOCALE_KEY, locale);
        }
        catch (NullPointerException ex) {
        }
    }

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

        // セッションに設定されたロケールを取得する。

        try {

            Locale sessionLocale = (Locale)getSession().getAttribute(FeatConst.FEATURE_CONTEXT_LOCALE_KEY);
            if ( sessionLocale != null )
                ret = new Locale[] {sessionLocale};

        }
        catch (NullPointerException ex) {
        }

        // セッションにロケールが設定されていなかった場合はHTTPヘッダの言語指定を取得する。

        if ( ret == null && request != null ) {

            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 = new Locale[languageRange.length];

                        for(int i=0; i<languageRange.length; i++) {
                            ret[i] = languageRange[i].lang;
                        }
                    }
                }
            }
        }

        if ( ret == null )
            ret = new Locale[] {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;
        }
    }
}
