/*
 * This software is distributed under following license based on modified BSD
 * style license.
 * ----------------------------------------------------------------------
 * 
 * Copyright 2009 The Nimbus2 Project. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE NIMBUS PROJECT ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL THE NIMBUS PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are
 * those of the authors and should not be interpreted as representing official
 * policies, either expressed or implied, of the Nimbus2 Project.
 */
package jp.ossc.nimbus.service.aop.interceptor;

import java.util.*;
import java.io.*;
import java.lang.reflect.InvocationTargetException;

import jp.ossc.nimbus.beans.*;
import jp.ossc.nimbus.core.*;
import jp.ossc.nimbus.util.*;
import jp.ossc.nimbus.service.aop.*;

import org.apache.commons.jexl2.Expression;
import org.apache.commons.jexl2.JexlEngine;
import org.apache.commons.jexl2.JexlContext;
import org.apache.commons.jexl2.MapContext;

/**
 * 例外処理インターセプタ。<p>
 *
 * @author M.Takata
 */
public class ExceptionHandlingInterceptorService extends ServiceBase
 implements Interceptor, ExceptionHandlingInterceptorServiceMBean{
    
    private static final long serialVersionUID = -5888057404214069278L;
    
    private static final String SERVLET_EXCEPTION_NAME = "javax.servlet.ServletException";
    private static final String GET_ROOT_CAUSE_METHOD = "getRootCause";
    private static final String JMS_EXCEPTION_NAME = "javax.jms.JMSException";
    private static final String GET_LINKED_EXCEPTION_METHOD = "getLinkedException";
    
    private static final String EHI__00001 = "EHI__00001";
    
    protected Map<String, String> exceptionAndHandlerMapping;
    protected ClassMappingTree<Object> exceptionMapForHandler;
    protected ServiceName defaultExceptionHandlerServiceName;
    protected ExceptionHandler defaultExceptionHandler;
    
    // ExceptionHandlingInterceptorServiceMBeanのJavaDoc
    public void setExceptionAndHandlerMapping(Map<String, String> map){
        exceptionAndHandlerMapping = map;
    }
    
    // ExceptionHandlingInterceptorServiceMBeanのJavaDoc
    public Map<String, String> getExceptionAndHandlerMapping(){
        return exceptionAndHandlerMapping;
    }
    
    // ExceptionHandlingInterceptorServiceMBeanのJavaDoc
    public void setDefaultExceptionHandlerServiceName(ServiceName name){
        defaultExceptionHandlerServiceName = name;
    }
    
    // ExceptionHandlingInterceptorServiceMBeanのJavaDoc
    public ServiceName getDefaultExceptionHandlerServiceName(){
        return defaultExceptionHandlerServiceName;
    }
    
    /**
     * サービスの開始処理を行う。<p>
     *
     * @exception Exception サービスの開始に失敗した場合
     */
    public void startService() throws Exception{
        if(exceptionAndHandlerMapping != null){
            exceptionMapForHandler = new ClassMappingTree<Object>();
            final ClassLoader loader = NimbusClassLoader.getInstance();
            final ServiceNameEditor editor = new ServiceNameEditor();
            editor.setServiceManagerName(getServiceManagerName());
            for(Map.Entry<String, String> entry : exceptionAndHandlerMapping.entrySet()){
                String exName = entry.getKey();
                final String name = entry.getValue();
                String condition = null;
                final int index = exName.indexOf('(');
                if(index != -1 && exName.charAt(exName.length() - 1) == ')'){
                    condition = exName.substring(
                        index + 1,
                        exName.length() - 1
                    );
                    exName = exName.substring(0, index);
                }
                final Class<?> clazz = Class.forName(exName, true, loader);
                editor.setAsText(name);
                final ServiceName serviceName = (ServiceName)editor.getValue();
                final ExceptionHandler handler = ServiceManagerFactory
                        .getServiceObject(serviceName);
                if(condition == null){
                    exceptionMapForHandler.add(
                        clazz,
                        handler
                    );
                }else{
                    Condition cond = new Condition(condition, handler);
                    try{
                        cond.evaluate(
                            (Throwable)clazz.newInstance(),
                            null,
                            true
                        );
                    }catch(InstantiationException e){
                    }catch(IllegalAccessException e){
                    }
                    exceptionMapForHandler.add(clazz, cond);
                }
            }
        }
        if(defaultExceptionHandlerServiceName != null){
            defaultExceptionHandler = ServiceManagerFactory
                .getServiceObject(defaultExceptionHandlerServiceName);
        }
    }
    
    /**
     * 次のインターセプタを呼び出し、ハンドリング対象の例外が発生すると例外処理を行うハンドラにフォワードする。<p>
     * サービスが開始されていない場合は、何もせずに次のインターセプタを呼び出す。<br>
     *
     * @param context 呼び出しのコンテキスト情報
     * @param chain 次のインターセプタを呼び出すためのチェーン
     * @return 呼び出し結果の戻り値
     * @exception Throwable 呼び出し先で例外が発生した場合、またはこのインターセプタで任意の例外が発生した場合。但し、本来呼び出される処理がthrowしないRuntimeException以外の例外をthrowしても、呼び出し元には伝播されない。
     */
    public Object invoke(
        InvocationContext context,
        InterceptorChain chain
    ) throws Throwable{
        if(getState() == State.STARTED){
            Object ret = null;
            try{
                ret = chain.invokeNext(context);
            }catch(Throwable th){
                boolean isHandled = false;
                ExceptionHandler handler = getTargetExceptionHandlerCause(
                    th,
                    context
                );
                if(handler != null){
                    // ハンドリング対象の例外
                    Throwable targetTh = getTargetException(
                        exceptionMapForHandler,
                        th,
                        context
                    );
                    if(targetTh == null){
                        targetTh = th;
                    }
                    ret = handler.handleException(targetTh, context);
                    isHandled = true;
                }
                if(!isHandled){
                    throw th;
                }
            }
            return ret;
        }else{
            return chain.invokeNext(context);
        }
    }
    
    /**
     * 指定された例外のハンドラクラスをマップから見つけて返す。<p>
     *
     * @param th 例外(ハンドリング対象の例外を含んでいる)
     * @param context 呼び出しのコンテキスト情報
     * @return 例外ハンドラ
     */
    protected ExceptionHandler getTargetExceptionHandlerCause(
        Throwable th,
        InvocationContext context
    ){
        ExceptionHandler handler = (ExceptionHandler)getTargetHandlerCause(
            exceptionMapForHandler,
            th,
            context
        );
        return handler == null ? defaultExceptionHandler : handler;
    }
    
    /**
     * 指定された例外のハンドラをマップから見つけて返す。<p>
     *
     * @param handlerMap ハンドラのマップ
     * @param th 例外(ハンドリング対象の例外を含んでいる)
     * @param context 呼び出しのコンテキスト情報
     * @return 例外ハンドラ
     */
    protected ExceptionHandler getTargetHandlerCause(
        ClassMappingTree<Object> handlerMap,
        Throwable th,
        InvocationContext context
    ){
        if(handlerMap == null){
            return null;
        }
        // 例外クラスに関連付いている例外ハンドラを取得
        List<Object> handlers = handlerMap.getValueList(th.getClass());
        if(handlers != null){
            for(int i = 0, imax = handlers.size(); i < imax; i++){
                Object handler = handlers.get(i);
                if(handler instanceof Condition){
                    Condition condition = (Condition)handler;
                    try{
                        if(condition.evaluate(th, context)){
                            return condition.handler;
                        }
                    }catch(Exception e){
                        getLogger().write(EHI__00001, e);
                    }
                }else{
                    return (ExceptionHandler)handler;
                }
            }
        }
        Throwable cause = getCause(th);
        return cause == null ? null : getTargetHandlerCause(handlerMap, cause, context);
    }
    
    /**
     * 指定された例外から、ハンドリング対象の例外を取り出す。<p>
     * 
     * @param handlerMap ハンドラのマップ
     * @param th 例外
     * @param context 呼び出しのコンテキスト情報
     * @return ハンドリング対象の例外
     */
    protected Throwable getTargetException(
        ClassMappingTree<Object> handlerMap,
        Throwable th,
        InvocationContext context
    ) {
        if(handlerMap == null){
            return th;
        }
        // 例外クラスに関連付いている例外ハンドラを取得
        List<Object> handlers = handlerMap.getValueList(th.getClass());
        if(handlers != null){
            for(int i = 0, imax = handlers.size(); i < imax; i++){
                Object handler = handlers.get(i);
                if(handler instanceof Condition){
                    Condition condition = (Condition)handler;
                    try{
                        if(condition.evaluate(th, context)){
                            return th;
                        }
                    }catch(Exception e){
                        getLogger().write(EHI__00001, e);
                    }
                }else{
                    return th;
                }
            }
        }
        
        Throwable cause = getCause(th);
        return cause == null ? null : getTargetException(
            handlerMap,
            cause,
            context
        );
    }
    
    /**
     * 指定された例外から原因を取得する。<p>
     *
     * @param th 例外
     * @return 原因
     */
    protected Throwable getCause(Throwable th){
        Throwable cause = null;
        if(th.getClass().getName().equals(SERVLET_EXCEPTION_NAME)){
            // 例外がServletExceptionの場合は、ルートの原因を取得
            try{
                cause = (Throwable)th.getClass()
                    .getMethod(GET_ROOT_CAUSE_METHOD).invoke(th);
            }catch(NoSuchMethodException e){
            }catch(IllegalAccessException e){
            }catch(InvocationTargetException e){
            }
        }else if(th.getClass().getName().equals(JMS_EXCEPTION_NAME)){
            // 例外がJMSExceptionの場合は、リンク例外を取得
            try{
                cause = (Exception)th.getClass()
                    .getMethod(GET_LINKED_EXCEPTION_METHOD).invoke(th);
            }catch(NoSuchMethodException e){
            }catch(IllegalAccessException e){
            }catch(InvocationTargetException e){
            }
        }else{
            cause = th.getCause();
        }
        return cause == th ? null : cause;
    }
    
    protected static class Condition implements Serializable{
        
        private static final long serialVersionUID = 3112081170501323189L;
        
        public static final String EXCEPTION_KEY = "exception";
        public static final String CONTEXT_KEY = "context";
        protected static final String PROP_FUNCTION_NAME = "prop";
        
        protected ExceptionHandler handler;
        
        protected transient Expression expression;
        protected String condition;
        
        public Condition(String cond, ExceptionHandler handler){
            initCondition(cond);
            condition = cond;
            this.handler = handler;
        }
        
        protected void initCondition(String cond){
            JexlEngine jexl = new JexlEngine();
            jexl.setSilent(true);
            Map<String, Object> funcs = new HashMap<String, Object>();
            PropertyAccess propAccess = new PropertyAccess();
            propAccess.setIgnoreNullProperty(true);
            funcs.put(PROP_FUNCTION_NAME, propAccess);
            jexl.setFunctions(funcs);
            expression = jexl.createExpression(cond);
        }
        
        public boolean evaluate(
            Throwable th,
            InvocationContext context
        ){
            return evaluate(th, context, false);
        }
        
        public boolean evaluate(
            Throwable th,
            InvocationContext context,
            boolean isTest
        ){
            JexlContext jexlContext = new MapContext();
            jexlContext.set(EXCEPTION_KEY, th);
            jexlContext.set(CONTEXT_KEY, context);
            
            Object exp = expression.evaluate(jexlContext);
            if(exp instanceof Boolean){
                return ((Boolean)exp).booleanValue();
            }else{
                if(exp == null && isTest){
                    return true;
                }
                throw new IllegalArgumentException(expression.getExpression());
            }
        }
        
        private void readObject(ObjectInputStream in)
         throws IOException, ClassNotFoundException{
            in.defaultReadObject();
            initCondition(condition);
        }
    }
}