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

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import feat2.config.ActionConfigBase;
import feat2.config.BeanConfig;
import feat2.config.ConfigurationException;
import feat2.config.DIComponentConfig;
import feat2.config.ExceptionConfig;
import feat2.config.InputConfigBase;
import feat2.config.InputLiteralConfig;
import feat2.config.InputObjectConfig;
import feat2.config.InputParamConfig;
import feat2.config.InputResourceConfig;
import feat2.config.ResponseConfigBase;
import feat2.config.ValidatableComponentConfig;
import feat2.config.ValidationConfig;
import feat2.config.type.JavaIdentifier;
import feat2.config.type.ObjectRef;
import feat2.config.type.Scope;
import feat2.impl.Command;
import feat2.impl.RedirectResponse;
import feat2.validation.ArrayValueValidator;
import feat2.validation.ObjectValidator;
import feat2.validation.SingleValueValidator;
import feat2.validation.Validator;

/**
 * featのアクションを実行する。
 * メソッドはスレッドセーフ。
 * @author SUGIMOTO Ken-ichi
 */
public class Processor {

    public Processor() {
    }

    public void process(CommandContext ctx) throws ServletException, IOException, ResponseException {

        try {

            // schemeが違ったらリダイレクト

            String scheme = ctx.getCurrentCommandConfig().getScheme();
            if ( scheme != null && !scheme.equals(ctx.getRequest().getScheme()) ) {

                changeScheme(ctx);
                return;

            }


            // コマンドの実行

            String responseName = null;
            Command command = new Command(ctx.getCurrentCommandConfig());
            try {

                responseName = processCommand(command, ctx);

            }
            catch (ApplicationException ex) {
                responseName = processException(ctx, ex);
            }



            // レスポンス

            HashSet responseHistory = new HashSet();

            while (true) {

                if ( responseName == null ) {
                    ResponseException ex = new ResponseException("exception.Processor.response.null");
                    ex.addKeyword("feature", ctx.getCurrentFeatureConfig().getName().getValue());
                    throw ex;
                }

                try {

                    // レスポンスの循環を調べる

                    if ( responseHistory.contains(responseName) ) {
                        ResponseException ex = new ResponseException("exception.Processor.response.recursion");
                        ex.addKeyword("feature", ctx.getCurrentFeatureConfig().getName().getValue());
                        ex.addKeyword("response", responseName);
                        throw ex;
                    }

                    responseHistory.add(responseName);

                    // レスポンスの処理

                    String next = processResponse(responseName, ctx);

                    if ( next == null )
                        break;
                    else
                        responseName = next;

                }
                catch (ApplicationException ex) {
                    responseName = processException(ctx, ex);
                }

            }

            // セッションスコープのbeanを上書きする

            processRewrite(command, ctx);

        }
        catch (PropertyAccessException ex) {
            throw new ServletException(ex);
        }
        catch (TemplateParsingException ex) {
            throw new ServletException(ex);
        }
        catch (NotHandledException ex) {
            throw new ServletException(ex.getCause());
        }
        catch (FileUploadException ex) {
            throw new ServletException(ex);
        }

    }


    private String processCommand(Command com, CommandContext ctx)
            throws PropertyAccessException, ApplicationException, FileUploadException {

        return com.execute(ctx);

    }


    /**
     *
     * @param responseName
     * @param ctx
     * @return レスポンスの委譲先。委譲しないときはnullを返す。
     * @throws IOException
     * @throws PropertyAccessException
     * @throws TemplateParsingException
     * @throws ServletException
     * @throws ApplicationException
     * @throws FileUploadException
     */
    private String processResponse(String responseName, CommandContext ctx)
            throws IOException, PropertyAccessException, TemplateParsingException, ServletException, ApplicationException, FileUploadException {

        // レスポンスのインスタンスを作る

        ResponseConfigBase responseConf = ctx.getCurrentFeatureConfig().getResponseConfig(responseName);
        Response res = responseConf.getResponseInstance();

        // レスポンスの処理を実行

        return res.output(ctx);

    }


    private String processException(CommandContext ctx, ApplicationException ex) throws NotHandledException {

        ctx.setException(ex.getCause());
        Class exclass = ex.getCause().getClass();
        ExceptionConfig exceptionConf = ctx.getCurrentCommandConfig().getException(exclass);

        if ( exceptionConf == null )
            exceptionConf = ctx.getCurrentFeatureConfig().getException(exclass);

        if ( exceptionConf == null )
            throw new NotHandledException(ex.getCause());

        String ret = exceptionConf.getResponseName().getValue();
        return ret;

    }


    /**
     * セッションスコープのBeanを上書きする。
     * @param command
     * @param ctx
     */
    private void processRewrite(Command command, CommandContext ctx) {

        ActionConfigBase[] actions = command.getConfig().getActions();

        for (int i = 0; i < actions.length; i++) {
            if ( actions[i] instanceof BeanConfig ) {
                BeanConfig beanConf = (BeanConfig)actions[i];
                Scope scope = beanConf.getScope();

                if ( scope.isSession() ) {
                    String beanName = beanConf.getName().getValue();
                    Object obj = ctx.getAttribute(beanName);
                    ctx.setAttribute(beanName, obj, scope);
                }

            }
        }

    }


    private void changeScheme(CommandContext ctx) throws IOException {

        try {

            // URIのスキームを変更

            HttpServletRequest request = ctx.getRequest();
            URI uri = new URI(request.getRequestURI());
            String scheme = ctx.getCurrentCommandConfig().getScheme();
            String method = request.getMethod().toLowerCase();

            String host = uri.getHost();
            if ( !uri.isAbsolute() ) {

                host = request.getHeader("Host");
                int colIdx = host.indexOf(':');
                if ( colIdx > -1 ) {
                    host = host.substring(0, colIdx);
                }

            }

            int port = -1;

            if ( scheme.equals("http") )
                port = ctx.getFeatConfig().getHttpPort();
            else if ( scheme.equals("https") )
                port = ctx.getFeatConfig().getHttpsPort();

            URI newUri = new URI(scheme, uri.getRawUserInfo(), host, port, uri.getRawPath(), uri.getRawQuery(), null);

            // リダイレクト

            if ( method.equals("get") ) {
                RedirectResponse.redirectGet(newUri.toString(), ctx.getRequestParameterMap(), ctx);
            }
            else if ( method.equals("post") ) {
                RedirectResponse.redirectPost(newUri.toString(), ctx.getRequestParameterMap(), ctx);
            }

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

    }


    // スタティックメソッド ---------------------------------------------------

    /**
     * 入力値を返す。
     */
    public static Map processInputs(DIComponentConfig config, CommandContext ctx)
        throws PropertyAccessException, FileUploadException {

        InputConfigBase[] inputs = config.getInputs();
        HashMap ret = new HashMap();

        HttpServletRequest req = ctx.getRequest();
        boolean isGetMethod = req.getMethod().equalsIgnoreCase("GET");
        String encoding = req.getCharacterEncoding();

        for(int i=0; i<inputs.length; i++) {

            // FIXME ソース値がnullのときの扱いをもっと詳細に
            // リテラルでソース値がnullだったら例外(ConfigException)

            Object inputValue = null;

            // オブジェクトの参照

            if ( inputs[i] instanceof InputObjectConfig ) {
                InputObjectConfig inputObjectConf = (InputObjectConfig)inputs[i];
                ObjectRef ref = inputObjectConf.getRef();

                // オブジェクトを見つける

                Object target = null;
                Scope scope = inputObjectConf.getScope();

                if ( scope == null )
                    scope = new Scope("local");

                target = ctx.getAttribute(ref.getName(), scope);

                // プロパティの値を取得する

                if ( target != null ) {
                    if ( ref.getNestedPropertyName() != null ) {
                        try {
                            inputValue = ObjectUtil.getProperty(target, ref.getNestedPropertyName());
                        }
                        catch (NoSuchMethodException ex) {
                            throw new PropertyAccessException(inputObjectConf.getConfigPath(), ex);
                        }
                        catch (InvocationTargetException ex) {
                            throw new PropertyAccessException(inputObjectConf.getConfigPath(), ex);
                        }
                        catch (IllegalAccessException ex) {
                            throw new PropertyAccessException(inputObjectConf.getConfigPath(), ex);
                        }
                    }
                    else {
                        inputValue = target;
                    }
                }

            }

            // リクエストパラメータからの入力

            else if ( inputs[i] instanceof InputParamConfig ) {

                InputParamConfig inputParamConf = (InputParamConfig)inputs[i];

                if ( ctx.isMultipartRequest() ) {

                    Map params = ctx.getMultipartRequestBody();
                    CommandContext.MultipartFormItem item = (CommandContext.MultipartFormItem)params.get(inputParamConf.getRef());
                    if ( item != null ) {
                        if ( item.isFormField() )
                            inputValue = item.getString();
                        else
                            inputValue = item;
                    }

                }

                else {

                    String[] values = ctx.getRequestParameterValues(inputParamConf.getRef());

                    if ( values != null ) {

                        if ( values.length == 1 )
                            inputValue = values[0];
                        else
                            inputValue = values;

                    }

                }

            }

            // リテラルからの入力

            else if ( inputs[i] instanceof InputLiteralConfig ) {
                InputLiteralConfig inputLiteralConf = (InputLiteralConfig)inputs[i];
                inputValue = inputLiteralConf.getValue();
            }

            // リソースからの入力

            else if ( inputs[i] instanceof InputResourceConfig ) {
                InputResourceConfig inputResourceConf = (InputResourceConfig)inputs[i];
                inputValue = ctx.getStringResource(inputResourceConf.getRef());
            }


            // 結果のマップに値をセット

            String propName = inputs[i].getPropertyName().getValue();
            ret.put(propName, inputValue);

        }

        return ret;
    }



    /**
     *
     * @param bean
     * @param inputValues
     * @param config
     * @param ctx
     * @throws PropertyAccessException
     * @throws FileUploadException
     */
    public static boolean processValidations(Object bean, Map inputValues, ValidatableComponentConfig config, CommandContext ctx)
        throws PropertyAccessException, FileUploadException {

        boolean ret = true;

        ValidationConfig[] validations = config.getValidations();
        HashMap results = new HashMap();
        FeatErrors errs = ctx.getFeatErrors();

        for (int i = 0; i < validations.length; i++) {

            // バリデータに引数をセット

            ValidationConfig validationConfig = validations[i];
            Validator validator = validationConfig.getValidatorInstance();

            Map validatorParams = processInputs(validationConfig, ctx);
            processInjection(validator, validatorParams);

            // 依存する値がすべて有効か調べる

            boolean valid = true;
            ValidationConfig[] depends = validationConfig.getDepends();
            for (int j = 0; j < depends.length; j++) {

                if ( results.get(depends[j].getName()) == null ) {
                    valid = false;
                    break;
                }

            }

            // 値を検証する

            JavaIdentifier[] propertyNames = validationConfig.getProperties();
            if ( valid ) {

                String messageId = validationConfig.getMessageId();
                if ( messageId == null )
                    messageId = validator.getDefaultMessageId();

                if ( validator instanceof ArrayValueValidator ) {

                    // 配列のまま検証

                    String[] values = getPropertyValues(bean, inputValues, propertyNames);

                    if ( ((ArrayValueValidator)validator).validate(values) ) {
                        results.put(validationConfig.getName(), "valid");
                    }
                    else {
                        ValidationError err = new ValidationError(messageId, validationConfig);
                        addKeywords(validator, err);
                        err.addKeyrowd("property", getPropertyDisplayName(propertyNames, config, errs.getResourceManager()));
                        errs.add(err);
                        ret = false;
                    }

                }

                else if ( validator instanceof SingleValueValidator ) {

                    // 値を一つずつ検証

                    String[] values = getPropertyValues(bean, inputValues, propertyNames);

                    int passCount = 0;
                    for (int j = 0; j < values.length; j++) {

                        if ( ((SingleValueValidator)validator).validate(values[j]) ) {
                            passCount++;
                        }
                        else {
                            ValidationError err = new ValidationError(messageId, validationConfig);
                            addKeywords(validator, err);
                            err.addKeyrowd("property", getPropertyDisplayName(propertyNames[j], config, errs.getResourceManager()));
                            errs.add(err);
                        }

                    }

                    // 値がすべて有効ならこのバリデーションの結果は有効

                    if ( passCount == values.length ) {
                        results.put(validationConfig.getName(), "valid");
                    }
                    else {
                        ret = false;
                    }

                }

                else if ( validator instanceof ObjectValidator ) {

                    // 値を一つずつ検証

                    Object[] values = getPropertyObjects(bean, inputValues, propertyNames);

                    int passCount = 0;
                    for (int j = 0; j < values.length; j++) {

                        if ( ((ObjectValidator)validator).validate(values[j]) ) {
                            passCount++;
                        }
                        else {
                            ValidationError err = new ValidationError(messageId, validationConfig);
                            addKeywords(validator, err);
                            err.addKeyrowd("property", getPropertyDisplayName(propertyNames[j], config, errs.getResourceManager()));
                            errs.add(err);
                        }

                    }

                    // 値がすべて有効ならこのバリデーションの結果は有効

                    if ( passCount == values.length ) {
                        results.put(validationConfig.getName(), "valid");
                    }
                    else {
                        ret = false;
                    }

                }

            }

        }

        return ret;

    }


    /**
     * 指定プロパティの値をMapの中から取得する。Mapに値がなければBeanから取得する。
     */
    private static String[] getPropertyValues(Object bean, Map inputValues, JavaIdentifier[] propertyNames) throws PropertyAccessException {

        Object[] objects = getPropertyObjects(bean, inputValues, propertyNames);
        ArrayList strings = new ArrayList();

        for (int i = 0; i < propertyNames.length; i++) {

            if ( objects[i] instanceof String[]  ) {
                String[] array = (String[])objects[i];
                for (int j = 0; j < array.length; j++) {
                    strings.add(array[j]);
                }
            }
            else if ( objects[i] instanceof String )
                strings.add(objects[i]);
            else
                strings.add(objects[i].toString());

        }

        return (String[])strings.toArray(new String[0]);

    }


    private static Object[] getPropertyObjects(Object bean, Map inputValues, JavaIdentifier[] propertyNames) throws PropertyAccessException {

        Object[] values = new Object[propertyNames.length];

        for (int i = 0; i < propertyNames.length; i++) {

            String propertyName = propertyNames[i].getValue();

            try {

                // 入力リストになかったらプロパティ値を取得

                Object v = inputValues.get(propertyName);
                if ( v == null )
                    v = ObjectUtil.getProperty(bean, propertyName);

                values[i] = v;

            }
            catch (NoSuchMethodException ex) {
                throw new PropertyAccessException(ex);
            }
            catch (InvocationTargetException ex) {
                throw new PropertyAccessException(ex);
            }
            catch (IllegalAccessException ex) {
                throw new PropertyAccessException(ex);
            }

        }

        return values;

    }


    private static String getPropertyDisplayName(JavaIdentifier propertyName, ValidatableComponentConfig config, ResourceManager rm) {
        String resourceId = config.getPropertyNameId(propertyName.getValue());
        String ret = null;

        if ( resourceId != null ) {
            ret = rm.getStringResource(resourceId);
            if ( ret == null )
                ret = resourceId;
        }

        if ( ret == null )
            ret = propertyName.getValue();

        return ret;
    }

    private static String getPropertyDisplayName(JavaIdentifier[] propertyNames, ValidatableComponentConfig config, ResourceManager rm) {

        StringBuffer buf = new StringBuffer();

        for (int i = 0; i < propertyNames.length; i++) {

            if ( buf.length() > 0 )
                buf.append(", ");
            buf.append(getPropertyDisplayName(propertyNames[i], config, rm));

        }

        return buf.toString();

    }

    /**
     * Validatorのキーワード置き換えリストをValidationErrorに追加する。
     */
    private static void addKeywords(Validator validator, ValidationError err) {

        Map kw = validator.getKeyrowds();

        if ( kw != null ) {

            for (Iterator iter = kw.keySet().iterator(); iter.hasNext();) {
                String key = (String) iter.next();
                err.addKeyrowd(key, (String)kw.get(key));
            }

        }

    }

    /**
     * コンバートとbeanへのセット。
     * @param bean
     * @param inputValues
     * @throws PropertyAccessException
     */
    public static void processInjection(Object bean, Map inputValues) throws PropertyAccessException {

        try {

            ObjectUtil.populate(bean, inputValues);

        }
        catch (IllegalAccessException ex) {
            throw new PropertyAccessException(ex);
        }
        catch (InvocationTargetException ex) {
            throw new PropertyAccessException(ex);
        }

    }


}
