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

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

import jp.ossc.nimbus.beans.*;
import jp.ossc.nimbus.core.*;
import jp.ossc.nimbus.beans.dataset.Record;
import jp.ossc.nimbus.beans.dataset.RecordList;
import jp.ossc.nimbus.beans.dataset.PropertySchema;
import jp.ossc.nimbus.service.sql.ConnectionFactory;
import jp.ossc.nimbus.service.sql.ConnectionFactoryException;
import jp.ossc.nimbus.service.sql.PersistentManager;
import jp.ossc.nimbus.service.sql.PersistentException;
import jp.ossc.nimbus.service.context.Context;
import jp.ossc.nimbus.service.codemaster.CodeMasterFinder;
import jp.ossc.nimbus.util.validator.Validator;
import jp.ossc.nimbus.util.validator.ValidateException;

/**
 * マスタバリデータ。<p>
 * DBのマスタに含まれているかを検証する。<br>
 * マスタの取得方法として２種類あり、マスタの更新頻度に応じて選択可能である。<br>
 * <p>
 * マスタの更新頻度が高い場合は、検証の都度マスタを検索する必要がある。<br>
 * そのような場合は、{@link #setQuery(String)}、{@link #setConnectionFactoryServiceName(ServiceName)}、{@link #setPersistentManagerServiceName(ServiceName)}を設定する。<br>
 * <p>
 * マスタの更新頻度が低い場合は、検証の都度マスタを検索する必要がない。<br>
 * このようなマスタを使用する場合、Nimbusでは通常{@link CodeMasterFinder コードマスタ}に、マスタRecordListを登録しておく事で、マスタを毎回検索しないようにする。<br>
 * その機能を利用する事で、検証の都度マスタを検索せずに、コードマスタ上のマスタRecordListに対して動的検索を行い検証することができる。<br>
 * コードマスタの取得方法には２種類あり、読み取り一貫性保障する場合は、スレッドコンテキストから取得する。その場合、{@link #setThreadContextServiceName(ServiceName)}、{@link #setCodeMasterThreadContextKey(String)}を設定する。<br>
 * また、読み取り一貫性保障をする必要がない場合は、直接{@link CodeMasterFinder}から取得する事も可能である。その場合、{@link #setCodeMasterFinderServiceName(ServiceName)}を設定する。<br>
 * 取得したコードマスタからマスタRecordListを特定するために、{@link #setCodeMasterName(String)}を設定する。<br>
 * マスタRecordListから動的検索を行う際の検索条件は、検証値が任意の条件に合致するレコードがあるかどうかを検証する事が可能で、その場合は、{@link #setSearchCondition(String)}、{@link #setBindDataMap(String, String)}を設定する。<br>
 * 検証対象の値が、マスタRecordListのプライマリキーの値で、合致するレコードがあるかどうかを検証する場合は、上記の設定は不要である。<br>
 * 
 * @author M.Takata
 */
public class MasterValidatorService extends ServiceBase
 implements Validator, MasterValidatorServiceMBean{
    
    private static final long serialVersionUID = 3833661471756025996L;
    private ServiceName connectionFactoryServiceName;
    private ConnectionFactory connectionFactory;
    private ServiceName persistentManagerServiceName;
    private PersistentManager persistentManager;
    private String query;
    
    private ServiceName codeMasterFinderServiceName;
    private CodeMasterFinder codeMasterFinder;
    
    private ServiceName threadContextServiceName;
    private Context<Object, Object> threadContext;
    private String codeMasterThreadContextKey
         = jp.ossc.nimbus.service.aop.interceptor.ThreadContextKey.CODEMASTER;
    
    private String codeMasterName;
    private String searchCondition;
    private Map<String, Property> bindDataMap;
    
    // MasterValidatorServiceMBeanのJavaDoc
    public void setConnectionFactoryServiceName(ServiceName name){
        connectionFactoryServiceName = name;
    }
    // MasterValidatorServiceMBeanのJavaDoc
    public ServiceName getConnectionFactoryServiceName(){
        return connectionFactoryServiceName;
    }
    
    // MasterValidatorServiceMBeanのJavaDoc
    public void setPersistentManagerServiceName(ServiceName name){
        persistentManagerServiceName = name;
    }
    // MasterValidatorServiceMBeanのJavaDoc
    public ServiceName getPersistentManagerServiceName(){
        return persistentManagerServiceName;
    }
    
    // MasterValidatorServiceMBeanのJavaDoc
    public void setQuery(String query){
        this.query = query;
    }
    // MasterValidatorServiceMBeanのJavaDoc
    public String getQuery(){
        return query;
    }
    
    // MasterValidatorServiceMBeanのJavaDoc
    public void setCodeMasterFinderServiceName(ServiceName name){
        codeMasterFinderServiceName = name;
    }
    // MasterValidatorServiceMBeanのJavaDoc
    public ServiceName getCodeMasterFinderServiceName(){
        return codeMasterFinderServiceName;
    }
    
    // MasterValidatorServiceMBeanのJavaDoc
    public void setThreadContextServiceName(ServiceName name){
        threadContextServiceName = name;
    }
    // MasterValidatorServiceMBeanのJavaDoc
    public ServiceName getThreadContextServiceName(){
        return threadContextServiceName;
    }
    
    // MasterValidatorServiceMBeanのJavaDoc
    public void setCodeMasterThreadContextKey(String key){
        codeMasterThreadContextKey = key;
    }
    // MasterValidatorServiceMBeanのJavaDoc
    public String getCodeMasterThreadContextKey(){
        return codeMasterThreadContextKey;
    }
    
    // MasterValidatorServiceMBeanのJavaDoc
    public void setCodeMasterName(String name){
        codeMasterName = name;
    }
    // MasterValidatorServiceMBeanのJavaDoc
    public String getCodeMasterName(){
        return codeMasterName;
    }
    
    // MasterValidatorServiceMBeanのJavaDoc
    public void setSearchCondition(String name){
        searchCondition = name;
    }
    // MasterValidatorServiceMBeanのJavaDoc
    public String getSearchCondition(){
        return searchCondition;
    }
    
    // MasterValidatorServiceMBeanのJavaDoc
    public void setBindDataMap(String key, String valueKey){
        if(bindDataMap == null){
            bindDataMap = new HashMap<String, Property>();
        }
        if(!valueKey.startsWith(BIND_DATA_VALUE_KEY)){
            throw new IllegalArgumentException("ValueKey must start with 'VALUE'.");
        }
        if(valueKey.equals(BIND_DATA_VALUE_KEY)){
            bindDataMap.put(key, null);
        }else{
            valueKey = valueKey.substring(BIND_DATA_VALUE_KEY.length());
            if(valueKey.charAt(0) == '.'){
                valueKey = valueKey.substring(1);
            }
            bindDataMap.put(key, PropertyFactory.createProperty(valueKey));
        }
    }
    // MasterValidatorServiceMBeanのJavaDoc
    public String getBindDataMap(String key){
        if(bindDataMap == null){
            return null;
        }
        final Property prop = (Property)bindDataMap.get(key);
        return prop == null ? BIND_DATA_VALUE_KEY
             : BIND_DATA_VALUE_KEY + '.' + prop.getPropertyName();
    }
    
    /**
     * サービスの開始処理を行う。<p>
     *
     * @exception Exception サービスの開始処理に失敗した場合
     */
    public void startService() throws Exception{
        if(connectionFactoryServiceName != null){
            connectionFactory = ServiceManagerFactory
                .getServiceObject(connectionFactoryServiceName);
            if(persistentManagerServiceName == null){
                throw new IllegalArgumentException("PersistentManagerServiceName must be specified.");
            }
            persistentManager = ServiceManagerFactory
                .getServiceObject(persistentManagerServiceName);
            
            if(query == null){
                throw new IllegalArgumentException("Query must be specified.");
            }
        }else{
            if(codeMasterFinderServiceName != null){
                codeMasterFinder = ServiceManagerFactory
                    .getServiceObject(codeMasterFinderServiceName);
            }
            if(threadContextServiceName != null){
                threadContext = ServiceManagerFactory
                    .getServiceObject(threadContextServiceName);
            }
            if(codeMasterFinder == null && threadContext == null){
                throw new IllegalArgumentException("It is necessary to set either of CodeMasterFinder or ThreadContext.");
            }
            if(codeMasterName == null){
                throw new IllegalArgumentException("CodeMasterName must be specified.");
            }
            if(searchCondition != null){
                if(bindDataMap == null || bindDataMap.size() == 0){
                    throw new IllegalArgumentException("BindDataMap must be specified.");
                }
            }
        }
    }
    
    /**
     * 指定されたオブジェクトがマスタに含まれているかを検証する。<p>
     *
     * @param obj 検証対象のオブジェクト
     * @return 検証結果。検証成功の場合true
     * @exception ValidateException 検証に失敗した場合
     */
    @SuppressWarnings("unchecked")
    public boolean validate(Object obj) throws ValidateException{
        if(connectionFactory != null){
            Connection con = null;
            try{
                con = connectionFactory.getConnection();
                final List<Map<String, Object>> result = (List<Map<String, Object>>)persistentManager.loadQuery(con, query, obj, null);
                if(result.size() == 0){
                    return false;
                }
                final Collection<Object> values = result.get(0).values();
                if(values.size() == 0){
                    return false;
                }
                if(values.size() == 1){
                    final Object value = values.iterator().next();
                    if(value instanceof Boolean){
                        return ((Boolean)value).booleanValue();
                    }else if(value instanceof Number){
                        return ((Number)value).intValue() == 1;
                    }else{
                        return true;
                    }
                }else{
                    return true;
                }
            }catch(ConnectionFactoryException e){
                throw new ValidateException(e);
            }catch(PersistentException e){
                throw new ValidateException(e);
            }finally{
                if(con != null){
                    try{
                        con.close();
                    }catch(SQLException e){
                    }
                }
            }
        }else{
            Map<String, Object> codeMaster = null;
            if(threadContext != null){
                codeMaster = (Map<String, Object>)threadContext.get(codeMasterThreadContextKey);
            }
            if(codeMaster == null && codeMasterFinder != null){
                codeMaster = codeMasterFinder.getCodeMasters();
            }
            if(codeMaster == null){
                throw new ValidateException("CodeMaster is not found.");
            }
            final Object master = codeMaster.get(codeMasterName);
            if(master == null){
                throw new ValidateException("Master '" + codeMasterName + "' is not found.");
            }
            if(!(master instanceof RecordList)){
                throw new ValidateException("Master '" + codeMasterName + "' is not RecordList.");
            }
            final RecordList recordList = (RecordList)master;
            if(searchCondition != null){
                final Map<String, Object> params = new HashMap<String, Object>();
                try{
                    for(Map.Entry<String, Property> entry : bindDataMap.entrySet()){
                        final String key = entry.getKey();
                        final Property prop = entry.getValue();
                        if(prop == null){
                            params.put(key, obj);
                        }else{
                            params.put(key, prop.getProperty(obj));
                        }
                    }
                }catch(NoSuchPropertyException e){
                    throw new ValidateException(e);
                }catch(InvocationTargetException e){
                    throw new ValidateException(e.getCause());
                }
                try{
                    return recordList.searchByCondition(
                        searchCondition,
                        params
                    ).size() != 0;
                }catch(Exception e){
                    throw new ValidateException(e);
                }
            }else{
                PropertySchema[] schemata = recordList.getRecordSchema().getPrimaryKeyPropertySchemata();
                if(schemata == null || schemata.length != 1){
                    throw new ValidateException("Size of primary key property not equal 1.");
                }
                Record key = recordList.createRecord();
                key.setProperty(schemata[0].getName(), obj);
                return recordList.searchByPrimaryKey(key) != null;
            }
        }
    }
}