/*
 * Copyright (c) 2011 NTT DATA Corporation
 */

package jp.terasoluna.batch.functionsample.b005;

import java.lang.reflect.Method;
import java.util.Map;

import jp.terasoluna.fw.batch.exception.BatchException;
import jp.terasoluna.fw.exception.SystemException;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * バッチの操作ログを記録する。
 * 
 */
public class OperationLoggerImpl implements OperationLogger {

    /**
     * Log。
     */
    private static Log log = LogFactory.getLog(OperationLoggerImpl.class);

    /**
     * 開始ログ.
     */
    private static int STATUS_START_LOG = 0;

    /**
     * 終了ログ.
     */
    private static int STATUS_END_LOG = 1;

    /**
     * 例外ログ.
     */
    private static int STATUS_EXCEPTION_LOG = 2;

    /**
     * ログレベル:fatal
     */
    private static String LOG_LEVEL_FATAL = "fatal";

    /**
     * ログレベル:error
     */
    private static String LOG_LEVEL_ERROR = "error";

    /**
     * ログレベル:warn
     */
    private static String LOG_LEVEL_WARN = "warn";

    /**
     * ログレベル:info
     */
    private static String LOG_LEVEL_INFO = "info";

    /**
     * ログレベル:debug
     */
    private static String LOG_LEVEL_DEBUG = "debug";

    /**
     * ログレベル:trace
     */
    private static String LOG_LEVEL_TRACE = "trace";

    /**
     * ログレベル:none （ログ出力しない）
     */
    private static String LOG_LEVEL_NONE = "none";

    /**
     * デフォルトログレベル
     */
    private static String DEFAULT_LOG_LEVEL = LOG_LEVEL_INFO;

    /**
     * システム例外エラーコード.
     */
    private static final String SYSTEM_ERROR_MESSAGE_ID = "errors.8004C999";

    /**
     * 開始ログログレベル
     */
    private String startLogLevel = DEFAULT_LOG_LEVEL;

    /**
     * 終了ログログレベル
     */
    private String endLogLevel = DEFAULT_LOG_LEVEL;

    /**
     * 例外ログログレベル（デフォルト）
     */
    private String exceptionLogLevel = DEFAULT_LOG_LEVEL;

    /**
     * 例外ログログレベル（例外ごとに指定）
     * key : 例外クラスの完全修飾名
     * value : ログレベル
     */
    private Map<String, String> exceptionLogLevelMap = null;

    /**
     * 開始ログのログレベル
     * 
     * @param startLogLevel the startLogLevel to set
     */
    public void setStartLogLevel(String startLogLevel) {
        this.startLogLevel = startLogLevel;
    }

    /**
     * 終了ログのログレベル
     * 
     * @param endLogLevel the endLogLevel to set
     */
    public void setEndLogLevel(String endLogLevel) {
        this.endLogLevel = endLogLevel;
    }

    /**
     * 例外ログのログレベル（デフォルト）
     * 
     * @param exceptionLogLevel the exceptionLogLevel to set
     */
    public void setExceptionLogLevel(String exceptionLogLevel) {
        this.exceptionLogLevel = exceptionLogLevel;
    }

    /**
     * 例外ログのログレベル（例外ごとに指定）
     * 
     * @param exceptionLogLevelMap the exceptionLogLevelMap to set
     */
    public void setExceptionLogLevelMap(Map<String, String> exceptionLogLevelMap) {
        this.exceptionLogLevelMap = exceptionLogLevelMap;
    }

    /**
     * DAOの開始直前にログを出力する。
     * 
     * @param clazz
     *            DAOのクラス名
     * @param method
     *            DAOのメソッド名
     * @param argument
     *            DAOの引き数の配列
     */
    public void logStartDAO(Class<?> clazz, Method method, Object[] argument) {
        outputLog(STATUS_START_LOG, clazz, method, argument, null, null,
                "DAO Start.");
    }

    /**
     * DAOの正常終了直後にログを出力する。
     * 
     * @param clazz
     *            DAOのクラス名
     * @param method
     *            DAOのメソッド名
     * @param argument
     *            DAOの引き数の配列
     * @param ret
     *            DAOの戻り値
     */
    public void logEndDAO(Class<?> clazz, Method method, Object[] argument,
            Object ret) {
        outputLog(STATUS_END_LOG, clazz, method, argument, ret, null,
                "DAO End.");
    }

    /**
     * DAOからの例外キャッチ時にログを出力する。
     * 
     * @param clazz
     *            DAOのクラス名
     * @param method
     *            DAOのメソッド名
     * @param argument
     *            DAOの引き数の配列
     * @param exception
     *            DAOからスローされた例外
     */
    public void logExceptionDAO(Class<?> clazz, Method method,
            Object[] argument, Throwable exception) {
        outputLog(STATUS_EXCEPTION_LOG, clazz, method, argument, null,
                exception, "DAO Exception.");
    }

    /**
     * BLogicの開始直前にログを出力する。
     * 
     * @param clazz
     *            BLogicのクラス名
     * @param method
     *            BLogicのメソッド名
     * @param argument
     *            BLogicの引き数の配列
     */
    public void logStartBLogic(Class<?> clazz, Method method, Object[] argument) {
        outputLog(STATUS_START_LOG, clazz, method, argument, null, null,
                "BLogic Start.");
    }

    /**
     * BLogicの正常終了直後にログを出力する。
     * 
     * @param clazz
     *            BLogicのクラス名
     * @param method
     *            BLogicのメソッド名
     * @param argument
     *            BLogicの引き数の配列
     * @param ret
     *            BLogicの戻り値
     */
    public void logEndBLogic(Class<?> clazz, Method method, Object[] argument,
            Object ret) {
        outputLog(STATUS_END_LOG, clazz, method, argument, ret, null,
                "BLogic End.");
    }

    /**
     * BLogicからの例外キャッチ時にログを出力する。
     * 
     * @param clazz
     *            BLogicのクラス名
     * @param method
     *            BLogicのメソッド名
     * @param argument
     *            BLogicの引き数の配列
     * @param exception
     *            BLogicからスローされた例外
     */
    public void logExceptionBLogic(Class<?> clazz, Method method,
            Object[] argument, Throwable exception) {
        outputLog(STATUS_EXCEPTION_LOG, clazz, method, argument, null,
                exception, "BLogic Exception.");
    }

    /**
     * BLogicの関するログを出力する。
     * @param logStat 
     * 
     * @param clazz
     *            BLogicのクラス名
     * @param method
     *            BLogicのメソッド名
     * @param argument
     *            BLogicの引き数の配列
     * @param ret
     *            BLogicの戻り値の配列(正常終了のタイミングで呼び出された場合のみ)
     * @param exception
     *            BLogicからスローされた例外(異常終了のタイミングで呼び出された場合のみ)
     * @param message
     *            メッセージ
     */
    protected void outputLog(int logStat, Class<?> clazz, Method method,
            Object[] argument, Object ret, Throwable exception, String message) {

        if (log.isTraceEnabled()) {
            log.trace("操作ログを出力処理開始");
            log.trace("clazz：" + clazz);
            log.trace("method：" + method);
            log.trace("argument：" + argument);
            log.trace("ret：" + ret);
            log.trace("exception：" + exception);
            log.trace("message：" + message);
        }

        // ログレベル決定
        String logLevel = DEFAULT_LOG_LEVEL;
        if (logStat == STATUS_START_LOG) {
            logLevel = startLogLevel;
        } else if (logStat == STATUS_END_LOG) {
            logLevel = endLogLevel;
        } else if (logStat == STATUS_EXCEPTION_LOG) {
            logLevel = exceptionLogLevel;

            // 例外ごとのログレベルが指定されている場合は取得する
            if (exceptionLogLevelMap != null && exception != null) {
                String className = exception.getClass().getName();
                if (exceptionLogLevelMap.containsKey(className)) {
                    logLevel = exceptionLogLevelMap.get(className);
                }
            }
        }

        // ログ出力するか判定
        if (isLogEnabled(logLevel)) {

            StringBuilder logData = new StringBuilder(message);
            logData.append(" [");

            // 開始ログ時はパラメータを出力
            if (logStat == STATUS_START_LOG && argument != null) {
                outputStartLogData(logData, argument);
            }

            // 終了ログ時は結果オブジェクトを出力
            if (logStat == STATUS_END_LOG && ret != null) {
                outputOutputLogData(logData, ret);
            }

            // 例外ログ時は例外情報を出力
            if (logStat == STATUS_EXCEPTION_LOG && exception != null) {
                // Log4J出力
                outputExceptionLogData(logData, exception);
            }
            logData.append("]");

            // ログ出力
            outputLog(logLevel, logData, exception);
        }

        if (log.isTraceEnabled()) {
            log.trace("操作ログを出力処理終了");
        }

    }

    /**
     * ログ出力するか判定
     * 
     * @param logLevel
     * @return
     */
    protected boolean isLogEnabled(String logLevel) {
        if (LOG_LEVEL_TRACE.equalsIgnoreCase(logLevel)) {
            if (log.isTraceEnabled()) {
                return true;
            }
        } else if (LOG_LEVEL_DEBUG.equalsIgnoreCase(logLevel)) {
            if (log.isDebugEnabled()) {
                return true;
            }
        } else if (LOG_LEVEL_INFO.equalsIgnoreCase(logLevel)) {
            if (log.isInfoEnabled()) {
                return true;
            }
        } else if (LOG_LEVEL_WARN.equalsIgnoreCase(logLevel)) {
            if (log.isWarnEnabled()) {
                return true;
            }
        } else if (LOG_LEVEL_ERROR.equalsIgnoreCase(logLevel)) {
            if (log.isErrorEnabled()) {
                return true;
            }
        } else if (LOG_LEVEL_FATAL.equalsIgnoreCase(logLevel)) {
            if (log.isFatalEnabled()) {
                return true;
            }
        } else if (LOG_LEVEL_NONE.equalsIgnoreCase(logLevel)) {
            return false;
        }
        return false;
    }

    /**
     * ログ出力
     * 
     * @param logLevel
     * @param logData
     * @param exception
     */
    protected void outputLog(String logLevel, StringBuilder logData,
            Throwable exception) {
        if (LOG_LEVEL_TRACE.equalsIgnoreCase(logLevel)) {
            if (log.isTraceEnabled()) {
                if (exception == null) {
                    log.trace(logData.toString());
                } else {
                    log.trace(logData.toString(), exception);
                }
            }
        } else if (LOG_LEVEL_DEBUG.equalsIgnoreCase(logLevel)) {
            if (log.isDebugEnabled()) {
                if (exception == null) {
                    log.debug(logData.toString());
                } else {
                    log.debug(logData.toString(), exception);
                }
            }
        } else if (LOG_LEVEL_INFO.equalsIgnoreCase(logLevel)) {
            if (log.isInfoEnabled()) {
                if (exception == null) {
                    log.info(logData.toString());
                } else {
                    log.info(logData.toString(), exception);
                }
            }
        } else if (LOG_LEVEL_WARN.equalsIgnoreCase(logLevel)) {
            if (log.isWarnEnabled()) {
                if (exception == null) {
                    log.warn(logData.toString());
                } else {
                    log.warn(logData.toString(), exception);
                }
            }
        } else if (LOG_LEVEL_ERROR.equalsIgnoreCase(logLevel)) {
            if (log.isErrorEnabled()) {
                if (exception == null) {
                    log.error(logData.toString());
                } else {
                    log.error(logData.toString(), exception);
                }
            }
        } else if (LOG_LEVEL_FATAL.equalsIgnoreCase(logLevel)) {
            if (log.isFatalEnabled()) {
                if (exception == null) {
                    log.fatal(logData.toString());
                } else {
                    log.fatal(logData.toString(), exception);
                }
            }
        } else if (LOG_LEVEL_NONE.equalsIgnoreCase(logLevel)) {
            // ログ出力しない
        }
    }

    /**
     * 開始ログ出力用データを作成する
     * 
     * @param logData
     * @param argument
     */
    protected void outputStartLogData(StringBuilder logData, Object[] argument) {
        // 引き数の取得
        for (int i = 0; i < argument.length; i++) {
            if (i != 0) {
                logData.append(",");
            }

            if (argument[i] != null) {
                // 引数が文字列の場合
                if (argument[i] instanceof String) {
                    logData.append((String) argument[i]);
                }
                // 引数がプリミティブの場合
                else if (argument[i].getClass().isPrimitive()) {
                    logData.append(argument[i]);
                }
                // 引数が上記以外の場合
                else {
                    Method toStringMethod = null;
                    try {
                        // toStringメソッドを取得
                        toStringMethod = argument[i].getClass().getMethod(
                                "toString");
                    } catch (Throwable e) {
                        // 例外は無視
                    }
                    Class<?> decClass = null;
                    if (toStringMethod != null) {
                        // toStringメソッドの実装クラスを取得
                        decClass = toStringMethod.getDeclaringClass();
                    }
                    // toStringメソッドが拡張されていればtoStringメソッドを利用する
                    if (!Object.class.equals(decClass)) {
                        logData.append(argument[i].toString());
                    } else {
                        // toStringメソッドがないので、ToStringBuilderを利用する
                        try {
                            String str = ToStringBuilder.reflectionToString(
                                    argument[i],
                                    ToStringStyle.SHORT_PREFIX_STYLE);
                            logData.append(str);
                        } catch (Throwable e) {
                            // 例外ならとりあえずtoString
                            logData.append(argument[i].toString());
                        }
                    }
                }
            }
        }
    }

    /**
     * 終了ログ出力用データを作成する
     * 
     * @param logData
     * @param ret
     */
    protected void outputOutputLogData(StringBuilder logData, Object ret) {
        Method toStringMethod = null;
        try {
            // toStringメソッドを取得
            toStringMethod = ret.getClass().getMethod("toString");
        } catch (Throwable e) {
            // 例外は無視
        }
        Class<?> decClass = null;
        if (toStringMethod != null) {
            // toStringメソッドの実装クラスを取得
            decClass = toStringMethod.getDeclaringClass();
        }
        // toStringメソッドが拡張されていればtoStringメソッドを利用する
        if (!Object.class.equals(decClass)) {
            logData.append(ret.toString());
        } else {
            // toStringメソッドがないので、ToStringBuilderを利用する
            try {
                String str = ToStringBuilder.reflectionToString(ret,
                        ToStringStyle.SHORT_PREFIX_STYLE);
                logData.append(str);
            } catch (Throwable e) {
                // 例外ならとりあえずtoString
                logData.append(ret.toString());
            }
        }
    }

    /**
     * 例外ログ出力用データを作成する
     * 
     * @param logData 
     * @param exception
     *            業務・システム・一般例外
     */
    protected void outputExceptionLogData(StringBuilder logData,
            Throwable exception) {
        String message = null;
        String msgcd = SYSTEM_ERROR_MESSAGE_ID;
        String options[] = null;

        if (exception instanceof SystemException) {
            SystemException e = (SystemException) exception;
            msgcd = e.getErrorCode();
            options = e.getOptions();
        } else if (exception instanceof BatchException) {
            BatchException e = (BatchException) exception;
            msgcd = e.getMessageId();
            message = e.getLogMessage();
        } else {
            message = exception.getMessage();
        }

        logData.append(getLogMsg(msgcd, message, options));
    }

    /**
     * ログメッセージを取得
     * 
     * @param msgId
     *            メッセージID
     * @param message
     *            メッセージ
     * @param options 
     * @return String 出力メッセージ
     */
    private static String getLogMsg(String msgId, String message,
            String[] options) {
        StringBuilder sb = new StringBuilder();
        sb.append(msgId);
        sb.append(":");
        sb.append(message);
        if (options != null) {
            sb.append("(");
            for (int i = 0; i < options.length; i++) {
                if (i != 0) {
                    sb.append(",");
                }
                sb.append(options[i]);
            }
            sb.append("(");
        }
        return sb.toString();
    }
}
