/*
 * 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.servlet;

import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.Iterator;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.servlet.http.*;

import jp.ossc.nimbus.beans.PropertyAccess;
import jp.ossc.nimbus.beans.NoSuchPropertyException;
import jp.ossc.nimbus.beans.dataset.RecordList;
import jp.ossc.nimbus.beans.dataset.Record;
import jp.ossc.nimbus.core.*;
import jp.ossc.nimbus.service.aop.*;
import jp.ossc.nimbus.service.codemaster.CodeMasterFinder;
import jp.ossc.nimbus.service.context.Context;

/**
 * 閉塞インターセプタ。<p>
 *
 * @author M.Takata
 */
public class BlockadeInterceptorService extends ServletFilterInterceptorService
 implements BlockadeInterceptorServiceMBean{
    
    private static final long serialVersionUID = -1694588353473833851L;
    
    private String requestObjectAttributeName
         = StreamExchangeInterceptorServiceMBean.DEFAULT_REQUEST_OBJECT_ATTRIBUTE_NAME;
    
    private Map<String,String> specialUserMapping;
    
    private ServiceName codeMasterFinderServiceName;
    private CodeMasterFinder codeMasterFinder;
    
    private ServiceName threadContextServiceName;
    private Context<?,?> threadContext;
    
    private String blockadeCodeMasterKey;
    private String specialUserCodeMasterKey;
    
    private String pathPropertyName = DEFAULT_PROPERTY_NAME_PATH;
    private String statePropertyName = DEFAULT_PROPERTY_NAME_STATE;
    private String messagePropertyName = DEFAULT_PROPERTY_NAME_MESSAGE;
    
    private PropertyAccess propertyAccess;
    private Map<String,Pattern> pathPatternMap;
    
    public void setRequestObjectAttributeName(String name){
        requestObjectAttributeName = name;
    }
    public String getRequestObjectAttributeName(){
        return requestObjectAttributeName;
    }
    
    public void setSpecialUserMapping(Map<String,String> mapping){
        specialUserMapping = mapping;
    }
    public Map<String,String> getSpecialUserMapping(){
        return specialUserMapping;
    }
    
    public void setCodeMasterFinderServiceName(ServiceName name){
        codeMasterFinderServiceName = name;
    }
    public ServiceName getCodeMasterFinderServiceName(){
        return codeMasterFinderServiceName;
    }
    
    public void setThreadContextServiceName(ServiceName name){
        threadContextServiceName = name;
    }
    public ServiceName getThreadContextServiceName(){
        return threadContextServiceName;
    }
    
    public void setBlockadeCodeMasterKey(String key){
        blockadeCodeMasterKey = key;
    }
    public String getBlockadeCodeMasterKey(){
        return blockadeCodeMasterKey;
    }
    
    public void setSpecialUserCodeMasterKey(String key){
        specialUserCodeMasterKey = key;
    }
    public String getSpecialUserCodeMasterKey(){
        return specialUserCodeMasterKey;
    }
    
    public void setPathPropertyName(String name){
        pathPropertyName = name;
    }
    public String getPathPropertyName(){
        return pathPropertyName;
    }
    
    public void setStatePropertyName(String name){
        statePropertyName = name;
    }
    public String getStatePropertyName(){
        return statePropertyName;
    }
    
    public void setMessagePropertyName(String name){
        messagePropertyName = name;
    }
    public String getMessagePropertyName(){
        return messagePropertyName;
    }
    
    public void setCodeMasterFinder(CodeMasterFinder finder){
        codeMasterFinder = finder;
    }
    
    public void setThreadContext(Context<?,?> context){
        threadContext = context;
    }
    
    /**
     * サービスの生成処理を行う。<p>
     *
     * @exception Exception サービスの生成に失敗した場合
     */
    public void createService() throws Exception{
        propertyAccess = new PropertyAccess();
        propertyAccess.setIgnoreNullProperty(true);
    }
    
    /**
     * サービスの開始処理を行う。<p>
     *
     * @exception Exception サービスの開始に失敗した場合
     */
    public void startService() throws Exception{
        if(codeMasterFinderServiceName == null && codeMasterFinder == null
            && threadContextServiceName == null && threadContext == null){
            throw new IllegalArgumentException("CodeMasterFinder or ThreadContext must be specified.");
        }
        if(blockadeCodeMasterKey == null){
            throw new IllegalArgumentException("BlockadeCodeMasterKey must be specified.");
        }
        if(specialUserCodeMasterKey != null && (specialUserMapping == null || specialUserMapping.size() == 0)){
            throw new IllegalArgumentException("SpecialUserMapping must be specified.");
        }
        if(codeMasterFinderServiceName != null){
            codeMasterFinder = ServiceManagerFactory.getServiceObject(codeMasterFinderServiceName);
        }
        if(threadContextServiceName != null){
            threadContext = ServiceManagerFactory.getServiceObject(threadContextServiceName);
        }
        pathPatternMap = null;
    }
    
    /**
     * サービスの破棄処理を行う。<p>
     *
     * @exception Exception サービスの破棄に失敗した場合
     */
    public void destroyService() throws Exception{
        propertyAccess = null;
    }
    
    /**
     * コードマスタの閉塞マスタ及び特権ユーザマスタをチェックして、閉塞状態の場合は例外をthrowする。<p>
     * サービスが開始されていない場合は、何もせずに次のインターセプタを呼び出す。<br>
     *
     * @param context 呼び出しのコンテキスト情報
     * @param chain 次のインターセプタを呼び出すためのチェーン
     * @return 呼び出し結果の戻り値
     * @exception Throwable 呼び出し先で例外が発生した場合、またはこのインターセプタで任意の例外が発生した場合。但し、本来呼び出される処理がthrowしないRuntimeException以外の例外をthrowしても、呼び出し元には伝播されない。
     */
    @SuppressWarnings("unchecked")
    public Object invokeFilter(
        ServletFilterInvocationContext context,
        InterceptorChain chain
    ) throws Throwable{
        if(getState() != State.STARTED){
            return chain.invokeNext(context);
        }
        final HttpServletRequest request = (HttpServletRequest)context.getServletRequest();
        String reqPath = request.getPathInfo();
        if(reqPath == null){
            reqPath = request.getServletPath();
        }
        Map<String,Object> codeMasters = null;
        if(codeMasterFinder != null){
            codeMasters = codeMasterFinder.getCodeMasters();
        }else{
            codeMasters = (Map<String,Object>)threadContext.get(ThreadContextKey.CODEMASTER);
        }
        if(codeMasters == null){
            throw new BlockadeProcessException("CodeMaster is null.");
        }
        Object blockadeCodeMaster = codeMasters.get(blockadeCodeMasterKey);
        if(blockadeCodeMaster == null){
            throw new BlockadeProcessException("BlockadeCodeMaster is null. key=" + blockadeCodeMasterKey);
        }
        Object specialUserCodeMaster = null;
        if(specialUserCodeMasterKey != null){
            specialUserCodeMaster = codeMasters.get(specialUserCodeMasterKey);
        }
        boolean isSpecialUser = false;
        String userKey = null;
        if(specialUserCodeMaster != null){
            Object requestObject = request.getAttribute(requestObjectAttributeName);
            if(requestObject == null){
                throw new BlockadeProcessException("RequestObject is null.");
            }
            if(specialUserCodeMaster instanceof RecordList){
                RecordList list = (RecordList)specialUserCodeMaster;
                Record primaryKey = list.createRecord();
                applySpecialUserMapping(requestObject, primaryKey);
                userKey = primaryKey.toString();
                isSpecialUser = list.searchByPrimaryKey(primaryKey) != null;
            }else{
                throw new BlockadeProcessException("Unsupported type of SpecialUserCodeMaster. type=" + specialUserCodeMaster.getClass());
            }
        }
        if(pathPatternMap == null){
            initPathPatternMap(blockadeCodeMaster);
        }
        if(blockadeCodeMaster instanceof List){
            List<?> list = (List<?>)blockadeCodeMaster;
            for(int i = 0, imax = list.size(); i < imax; i++){
                checkBlockade(reqPath, list.get(i), isSpecialUser, userKey);
            }
        }else{
            throw new BlockadeProcessException("Unsupported type of BlockadeCodeMaster. type=" + blockadeCodeMaster.getClass());
        }
        return chain.invokeNext(context);
    }
    
    private Object applySpecialUserMapping(Object requestObject, Object primaryKey) throws BlockadeProcessException{
        Iterator<Map.Entry<String,String>> entries = specialUserMapping.entrySet().iterator();
        while(entries.hasNext()){
            Map.Entry<String,String> entry = entries.next();
            Object key = null;
            try{
                key = propertyAccess.get(
                    requestObject, 
                    entry.getKey()
                );
            }catch(IllegalArgumentException e){
                throw new BlockadeProcessException("SpecialUserCodeMaster value '" + entry.getKey() + "' cannot acquire from a request.", e);
            }catch(NoSuchPropertyException e){
                throw new BlockadeProcessException("SpecialUserCodeMaster value '" + entry.getKey() + "' cannot acquire from a request.", e);
            }catch(InvocationTargetException e){
                throw new BlockadeProcessException("SpecialUserCodeMaster value '" + entry.getKey() + "' cannot acquire from a request.", e.getTargetException());
            }
            try{
                propertyAccess.set(
                    primaryKey,
                    entry.getValue(),
                    key
                );
            }catch(IllegalArgumentException e){
                throw new BlockadeProcessException("SpecialUserCodeMaster value '" + entry.getKey() + "' cannot set to a record.", e);
            }catch(NoSuchPropertyException e){
                throw new BlockadeProcessException("SpecialUserCodeMaster value '" + entry.getKey() + "' cannot set to a record.", e);
            }catch(InvocationTargetException e){
                throw new BlockadeProcessException("SpecialUserCodeMaster value '" + entry.getKey() + "' cannot set to a record.", e.getTargetException());
            }
        }
        return primaryKey;
    }
    
    private synchronized void initPathPatternMap(Object blockadeCodeMaster) throws BlockadeProcessException{
        if(pathPatternMap != null){
            return;
        }
        pathPatternMap = new HashMap<String,Pattern>();
        if(blockadeCodeMaster instanceof List){
            List<?> list = (List<?>)blockadeCodeMaster;
            for(int i = 0, imax = list.size(); i < imax; i++){
                initPathPatternMap(pathPatternMap, list.get(i));
            }
        }else{
            throw new BlockadeProcessException("Unsupported type of BlockadeCodeMaster. type=" + blockadeCodeMaster.getClass());
        }
    }
    
    private synchronized void initPathPatternMap(Map<String,Pattern> map, Object blockade) throws BlockadeProcessException{
        String path = null;
        try{
            path = (String)propertyAccess.get(
                blockade, 
                pathPropertyName
            );
        }catch(ClassCastException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' cannot acquire from a record.", e);
        }catch(IllegalArgumentException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' cannot acquire from a record.", e);
        }catch(NoSuchPropertyException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' cannot acquire from a record.", e);
        }catch(InvocationTargetException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' cannot acquire from a record.", e.getTargetException());
        }
        if(path == null){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' is null from a record.");
        }
        initPathPatternMap(map, path);
    }
    
    private synchronized void initPathPatternMap(Map<String,Pattern> map, String path) throws BlockadeProcessException{
        if(map.get(path) != null){
            return;
        }
        try{
            map.put(path, Pattern.compile(path));
        }catch(PatternSyntaxException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' cannot compile.", e);
        }
    }
    
    private void checkBlockade(String reqPath, Object blockade, boolean isSpecialUser, String userKey) throws BlockadeException, BlockadeProcessException{
        int state = 0;
        try{
            Object stateObject = propertyAccess.get(
                blockade, 
                statePropertyName
            );
            if(stateObject == null){
                throw new BlockadeProcessException("BlockadeCodeMaster value '" + statePropertyName + "' is null from a record.");
            }
            if(stateObject instanceof Number){
                state = ((Number)stateObject).intValue();
            }else if(stateObject instanceof String){
                state = Integer.parseInt((String)stateObject);
            }else if(stateObject instanceof Boolean){
                state = ((Boolean)stateObject).booleanValue() ? 1 : 0;
            }
        }catch(NumberFormatException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + statePropertyName + "' cannot acquire from a record.", e);
        }catch(IllegalArgumentException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + statePropertyName + "' cannot acquire from a record.", e);
        }catch(NoSuchPropertyException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + statePropertyName + "' cannot acquire from a record.", e);
        }catch(InvocationTargetException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + statePropertyName + "' cannot acquire from a record.", e.getTargetException());
        }
        if(state == BLOCKADE_STATE_OPEN){
            return;
        }
        String path = null;
        try{
            path = (String)propertyAccess.get(
                blockade, 
                pathPropertyName
            );
        }catch(ClassCastException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' cannot acquire from a record.", e);
        }catch(IllegalArgumentException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' cannot acquire from a record.", e);
        }catch(NoSuchPropertyException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' cannot acquire from a record.", e);
        }catch(InvocationTargetException e){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' cannot acquire from a record.", e.getTargetException());
        }
        if(path == null){
            throw new BlockadeProcessException("BlockadeCodeMaster value '" + pathPropertyName + "' is null from a record.");
        }
        boolean isMatch = false;
        if(reqPath.equals(path)){
            isMatch = true;
        }else{
            Pattern pattern = pathPatternMap.get(path);
            if(pattern == null){
                initPathPatternMap(pathPatternMap, path);
                pattern = pathPatternMap.get(path);
            }
            isMatch = pattern.matcher(reqPath).matches();
        }
        if(!isMatch){
            return;
        }
        switch(state){
        case BLOCKADE_STATE_TEST_OPEN:
            if(isSpecialUser){
                return;
            }
            throw new BlockadeException("Blockade because of not special user. user=" + userKey);
        case BLOCKADE_STATE_CLOSE:
        default:
            throw new BlockadeException("Blockade.");
        }
    }
}