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

import java.util.*;
import java.text.*;
import java.io.*;
import java.beans.PropertyEditor;

import jp.ossc.nimbus.beans.*;
import jp.ossc.nimbus.core.*;
import jp.ossc.nimbus.daemon.*;
import jp.ossc.nimbus.io.*;
import jp.ossc.nimbus.service.sequence.*;

/**
 * デフォルトスケジュール管理。<p>
 * メモリ中でスケジュールを作成・管理し、実行すべきスケジュールを提供する。<br>
 * {@link #setPersistDir(String)}を設定すれば、スケジュールの状態をファイルに永続化する事ができる。<br>
 *
 * @author M.Takata
 */
public class DefaultScheduleManagerService extends ServiceBase
 implements ScheduleManager, DefaultScheduleManagerServiceMBean{
    
    private static final long serialVersionUID = 3176394103850131069L;
    protected static final String DATE_DIR_FORMAT = "yyyyMMdd";
    protected static final String DATE_CSV_FORMAT = "yyyy/MM/dd HH:mm:ss SSS";
    protected static final String LOCAL_SEQUENCE_NUMBER_FILE = "sequence_number";
    protected static final String ARRAY_CLASS_SUFFIX = "[]";
    
    protected ServiceName[] scheduleMasterServiceNames;
    protected Map<String, ScheduleMaster> addedScheduleMasters;
    protected Map<String, ScheduleMaster> scheduleMasters;
    
    protected Properties scheduleMakerTypeMapping;
    protected Map<String, ScheduleMaker> addedScheduleMakerMap;
    protected Map<String, ScheduleMaker> scheduleMakerMap;
    protected ServiceName defaultScheduleMakerServiceName;
    protected ScheduleMaker defaultScheduleMaker;
    
    protected boolean isMakeScheduleOnStart = true;
    protected Map<Date, List<Schedule>> scheduleDateMap;
    protected Map<String, Schedule> scheduleMap;
    protected Map<String, Set<String>> scheduleDependedMap;
    protected List<Schedule> scheduleList;
    
    protected Map<String, Set<Schedule>> scheduleMasterMap;
    
    protected ServiceName sequenceServiceName;
    protected Sequence sequence;
    protected long sequenceNumber;
    protected Object sequenceNumberLock = "sequenceNumberLock";
    
    protected Set<ScheduleControlListener> scheduleControlListeners;
    
    protected String persistDir;
    
    protected long timeoverCheckInterval = 1000l;
    protected Daemon timeoverChecker;
    
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public void setDefaultScheduleMakerServiceName(ServiceName name){
        defaultScheduleMakerServiceName = name;
    }
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public ServiceName getDefaultScheduleMakerServiceName(){
        return defaultScheduleMakerServiceName;
    }
    
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public void setScheduleMasterServiceNames(ServiceName[] names){
        scheduleMasterServiceNames = names;
    }
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public ServiceName[] getScheduleMasterServiceNames(){
        return scheduleMasterServiceNames;
    }
    
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public void setScheduleMakerTypeMapping(Properties mapping){
        scheduleMakerTypeMapping = mapping;
    }
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public Properties getScheduleMakerTypeMapping(){
        return scheduleMakerTypeMapping;
    }
    
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public void setMakeScheduleOnStart(boolean isMake){
        isMakeScheduleOnStart = isMake;
    }
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public boolean isMakeScheduleOnStart(){
        return isMakeScheduleOnStart;
    }
    
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public void setSequenceServiceName(ServiceName name){
        sequenceServiceName = name;
    }
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public ServiceName getSequenceServiceName(){
        return sequenceServiceName;
    }
    
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public void setPersistDir(String dir){
        persistDir = dir;
    }
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public String getPersistDir(){
        return persistDir;
    }
    
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public void setTimeoverCheckInterval(long interval){
        timeoverCheckInterval = interval;
    }
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public long getTimeoverCheckInterval(){
        return timeoverCheckInterval;
    }
    
    /**
     * サービスの生成処理を行う。<p>
     *
     * @exception Exception サービスの生成処理に失敗した場合
     */
    public void createService() throws Exception{
        scheduleMakerMap = new HashMap<String, ScheduleMaker>();
        scheduleMasters = new HashMap<String, ScheduleMaster>();
        scheduleDateMap = Collections.synchronizedMap(new TreeMap<Date, List<Schedule>>());
        scheduleMap = Collections.synchronizedMap(new HashMap<String, Schedule>());
        scheduleDependedMap = Collections.synchronizedMap(new HashMap<String, Set<String>>());
        scheduleList = Collections.synchronizedList(new ArrayList<Schedule>());
        scheduleMasterMap = Collections.synchronizedMap(new HashMap<String, Set<Schedule>>());
        scheduleControlListeners = Collections.synchronizedSet(new LinkedHashSet<ScheduleControlListener>());
        addedScheduleMakerMap = null;
        addedScheduleMasters = null;
    }
    
    /**
     * サービスの開始処理を行う。<p>
     *
     * @exception Exception サービスの開始処理に失敗した場合
     */
    public void startService() throws Exception{
        
        if(addedScheduleMasters != null){
            scheduleMasters.putAll(addedScheduleMasters);
        }
        if(scheduleMasterServiceNames != null){
            for(int i = 0; i < scheduleMasterServiceNames.length; i++){
                final ScheduleMaster scheduleMaster
                    = (ScheduleMaster)ServiceManagerFactory
                        .getServiceObject(scheduleMasterServiceNames[i]);
                scheduleMasters.put(scheduleMaster.getId(), scheduleMaster);
            }
        }
        
        if(addedScheduleMakerMap != null){
            scheduleMakerMap.putAll(addedScheduleMakerMap);
        }
        if(scheduleMakerTypeMapping != null
             && scheduleMakerTypeMapping.size() != 0){
            final ServiceNameEditor editor = new ServiceNameEditor();
            editor.setServiceManagerName(getServiceManagerName());
            for(Map.Entry<Object, Object> entry : scheduleMakerTypeMapping.entrySet()){
                editor.setAsText((String)entry.getValue());
                final ServiceName scheduleMakerServiceName
                    = (ServiceName)editor.getValue();
                final ScheduleMaker scheduleMaker
                    = ServiceManagerFactory.getServiceObject(scheduleMakerServiceName);
                if(scheduleMakerMap.containsKey(entry.getKey())){
                    throw new IllegalArgumentException(
                        "Dupulicate scheduleMakerTypeMapping : "
                            + entry.getKey()
                    );
                }
                scheduleMakerMap.put((String)entry.getKey(), scheduleMaker);
            }
        }
        
        if(defaultScheduleMakerServiceName != null){
            defaultScheduleMaker = ServiceManagerFactory
                .getServiceObject(defaultScheduleMakerServiceName);
        }
        if(defaultScheduleMaker == null){
            final DefaultScheduleMakerService defaultScheduleMakerService
                = new DefaultScheduleMakerService();
            defaultScheduleMakerService.create();
            defaultScheduleMakerService.start();
            defaultScheduleMaker = defaultScheduleMakerService;
        }
        
        if(sequenceServiceName != null){
            sequence = ServiceManagerFactory
                .getServiceObject(sequenceServiceName);
        }
        
        loadLocalSequenceNumber();
        loadSchedules();
        
        if(isMakeScheduleOnStart){
            final Date now = new Date();
            final List<Schedule> oldScheduleList = findSchedules(now);
            if(oldScheduleList == null || oldScheduleList.size() == 0){
                makeSchedule(now);
            }
        }
        
        if(timeoverCheckInterval > 0){
            timeoverChecker = new Daemon(new TimeoverChecker());
            timeoverChecker.setName("Nimbus SchedulerManagerTimeoverChecker " + getServiceNameObject());
            timeoverChecker.start();
        }
    }
    
    /**
     * サービスの停止処理を行う。<p>
     *
     * @exception Exception サービスの停止処理に失敗した場合
     */
    public void stopService() throws Exception{
        if(timeoverChecker != null){
            timeoverChecker.stop();
            timeoverChecker = null;
        }
        if(scheduleMasters != null){
            scheduleMasters.clear();
        }
        if(scheduleMakerMap != null){
            scheduleMakerMap.clear();
        }
        if(scheduleMap != null){
            synchronized(scheduleMap){
                scheduleMap.clear();
            }
        }
        if(scheduleDependedMap != null){
            synchronized(scheduleDependedMap){
                scheduleDependedMap.clear();
            }
        }
        if(scheduleDateMap != null){
            synchronized(scheduleDateMap){
                scheduleDateMap.clear();
            }
        }
        if(scheduleList != null){
            synchronized(scheduleList){
                scheduleList.clear();
            }
        }
        if(scheduleMasterMap != null){
            synchronized(scheduleMasterMap){
                scheduleMasterMap.clear();
            }
        }
        sequenceNumber = 0;
    }
    
    /**
     * サービスの破棄処理を行う。<p>
     *
     * @exception Exception サービスの破棄処理に失敗した場合
     */
    public void destroyService() throws Exception{
        scheduleMakerMap = null;
        scheduleMasters = null;
        if(scheduleMap != null){
            scheduleMap = null;
        }
        if(scheduleDependedMap != null){
            scheduleDependedMap = null;
        }
        if(scheduleDateMap != null){
            scheduleDateMap = null;
        }
        if(scheduleList != null){
            scheduleList = null;
        }
        if(scheduleMasterMap != null){
            scheduleMasterMap = null;
        }
        if(scheduleControlListeners != null){
            scheduleControlListeners = null;
        }
        addedScheduleMakerMap = null;
        addedScheduleMasters = null;
    }
    
    /**
     * スケジュールのIDを生成する{@link Sequence}を設定する。<p>
     *
     * @param sequence Sequence
     */
    public void setSequence(Sequence sequence){
        this.sequence = sequence;
    }
    
    /**
     * スケジュールのIDを生成する{@link Sequence}を取得する。<p>
     *
     * @return Sequence
     */
    public Sequence getSequence(){
        return sequence;
    }
    
    /**
     * スケジュールマスタを登録する。<p>
     *
     * @param master スケジュールマスタ
     */
    public void addScheduleMaster(ScheduleMaster master){
        if(addedScheduleMasters == null){
            addedScheduleMasters = new HashMap<String, ScheduleMaster>();
        }
        if(addedScheduleMasters.containsKey(master.getId())){
            throw new IllegalArgumentException(
                "Dupulicate id : " + master.getId()
            );
        }
        addedScheduleMasters.put(master.getId(), master);
    }
    
    // ScheduleManagerのJavaDoc
    public void setScheduleMaker(String scheduleType, ScheduleMaker maker)
     throws IllegalArgumentException{
        if(addedScheduleMakerMap == null){
            addedScheduleMakerMap = new HashMap<String, ScheduleMaker>();
        }
        if(addedScheduleMakerMap.containsKey(scheduleType)){
            throw new IllegalArgumentException(
                "Dupulicate scheduleType : " + scheduleType
            );
        }
        addedScheduleMakerMap.put(scheduleType, maker);
    }
    
    // ScheduleManagerのJavaDoc
    public ScheduleMaker getScheduleMaker(String scheduleType){
        return scheduleMakerMap.get(scheduleType);
    }
    
    // ScheduleManagerのJavaDoc
    public void setDefaultScheduleMaker(ScheduleMaker maker){
        defaultScheduleMaker = maker;
    }
    
    // ScheduleManagerのJavaDoc
    public ScheduleMaker getDefaultScheduleMaker(){
        return defaultScheduleMaker;
    }
    
    // ScheduleManagerのJavaDoc
    public void makeSchedule(Date date) throws ScheduleMakeException{
        if(scheduleMasters.size() == 0){
            return;
        }
        final Date standardDate = getStandardTimeDate(
            date == null ? new Date() : date
        );
        final Iterator<ScheduleMaster> masters = scheduleMasters.values().iterator();
        final List<Schedule> tmpScheduleList = new ArrayList<Schedule>();
        final Map<String, Set<Schedule>> tmpScheduleMasterMap = new HashMap<String, Set<Schedule>>();
        while(masters.hasNext()){
            final ScheduleMaster master = (ScheduleMaster)masters.next().clone();
            ScheduleMaker maker = scheduleMakerMap.get(
                master.getScheduleType()
            );
            if(maker == null){
                maker = defaultScheduleMaker;
            }
            if(!master.isEnabled()){
                continue;
            }
            final Schedule[] schedules = maker.makeSchedule(
                standardDate,
                master
            );
            if(schedules == null || schedules.length == 0){
                continue;
            }
            
            final Set<Schedule> scheduleSet = Collections.synchronizedSet(new HashSet<Schedule>());
            for(int i = 0; i < schedules.length; i++){
                tmpScheduleList.add(schedules[i]);
                scheduleSet.add(schedules[i]);
            }
            tmpScheduleMasterMap.put(master.getId(), scheduleSet);
        }
        Collections.sort(tmpScheduleList);
        for(int i = 0, imax = tmpScheduleList.size(); i < imax; i++){
            final Schedule schedule = tmpScheduleList.get(i);
            addSchedule(schedule, true, true);
        }
    }
    
    /**
     * 指定された日付で基準時間のDateを取得する。<p>
     * 
     * @param date 日付
     * @return 基準時間のDate
     */
    protected Date getStandardTimeDate(Date date){
        final Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime();
    }
    
    /**
     * スケジュールIDを発番する。<p>
     *
     * @return スケジュールID
     * @exception ScheduleManageException スケジュールIDの発番に失敗した場合
     */
    protected String createScheduleId() throws ScheduleManageException{
        if(sequence == null){
            synchronized(sequenceNumberLock){
                sequenceNumber++;
                if(sequenceNumber < 0){
                    sequenceNumber = 1;
                }
            }
            if(persistDir != null){
                final File baseDir = new File(persistDir);
                if(!baseDir.exists()){
                    synchronized(persistDir){
                        if(!baseDir.exists()){
                            if(!baseDir.mkdirs()){
                                throw new ScheduleManageException("PersistDir can't make." + baseDir.getAbsolutePath());
                            }
                        }
                    }
                }
                synchronized(sequenceNumberLock){
                    final File file = new File(
                        baseDir,
                        LOCAL_SEQUENCE_NUMBER_FILE
                    );
                    BufferedWriter writer = null;
                    try{
                        if(!file.exists()){
                            file.createNewFile();
                        }
                        writer = new BufferedWriter(new FileWriter(file));
                        writer.write(String.valueOf(sequenceNumber));
                    }catch(IOException e){
                        throw new ScheduleManageException(e);
                    }finally{
                        if(writer != null){
                            try{
                                writer.close();
                            }catch(IOException e){
                                throw new ScheduleManageException(e);
                            }
                        }
                    }
                }
            }
            return String.valueOf(sequenceNumber);
        }else{
            return sequence.increment();
        }
    }
    
    // ScheduleManagerのJavaDoc
    public List<ScheduleMaster> findAllScheduleMasters() throws ScheduleManageException{
        List<ScheduleMaster> result = new ArrayList<ScheduleMaster>();
        if(scheduleMasters.size() == 0){
            return result;
        }
        synchronized(scheduleMasters){
            result.addAll(scheduleMasters.values());
        }
        Collections.sort(result);
        return result;
    }
    
    // ScheduleManagerのJavaDoc
    public ScheduleMaster findScheduleMaster(String id) throws ScheduleManageException{
        return scheduleMasters.get(id);
    }
    
    // ScheduleManagerのJavaDoc
    public List<Schedule> findAllSchedules() throws ScheduleManageException{
        synchronized(scheduleList){
            return new ArrayList<Schedule>(scheduleList);
        }
    }
    
    // ScheduleManagerのJavaDoc
    public Schedule findSchedule(String id) throws ScheduleManageException{
        return scheduleMap.get(id);
    }
    
    // ScheduleManagerのJavaDoc
    public List<Schedule> findSchedules(String masterId) throws ScheduleManageException{
        final List<Schedule> result = new ArrayList<Schedule>();
        final Set<Schedule> scheduleSet = scheduleMasterMap.get(masterId);
        if(scheduleSet == null){
            return result;
        }
        synchronized(scheduleSet){
            result.addAll(scheduleSet);
        }
        Collections.sort(result);
        return result;
    }
    
    // ScheduleManagerのJavaDoc
    public List<Schedule> findSchedules(Date date) throws ScheduleManageException{
        final Date standardDate = getStandardTimeDate(date);
        final List<Schedule> scheduleList = scheduleDateMap.get(standardDate);
        if(scheduleList == null){
            return new ArrayList<Schedule>();
        }else{
            synchronized(scheduleList){
                return new ArrayList<Schedule>(scheduleList);
            }
        }
    }
    
    // ScheduleManagerのJavaDoc
    public List<Schedule> findSchedules(Date from, Date to)
     throws ScheduleManageException{
        return findSchedules(from, to, null);
    }
    
    // ScheduleManagerのJavaDoc
    public List<Schedule> findSchedules(int[] states)
     throws ScheduleManageException{
        final List<Schedule> result = new ArrayList<Schedule>();
        
        Schedule[] schedules = null;
        synchronized(scheduleList){
            schedules = scheduleList.toArray(
                new Schedule[scheduleList.size()]
            );
        }
        for(int i = 0; i < schedules.length; i++){
            final Schedule schedule = schedules[i];
            for(int j = 0; j < states.length; j++){
                if(schedule.getState() == states[j]){
                    result.add(schedule);
                    break;
                }
            }
        }
        return result;
    }
    
    // ScheduleManagerのJavaDoc
    public List<Schedule> findSchedules(Date from, Date to, int[] states)
     throws ScheduleManageException{
        return findSchedules(from, to, states, null, null, null);
    }
    
    // ScheduleManagerのJavaDoc
    public List<Schedule> findSchedules(Date from, Date to, int[] states, String masterId) throws ScheduleManageException{
        return findSchedules(from, to, states, masterId, null, null);
    }
    
    /**
     * 指定された期間、指定された状態、指定された実行キーのスケジュールを検索する。<p>
     *
     * @param from 期間の開始日時
     * @param to 期間の終了日時
     * @param states スケジュール状態の配列
     * @param executorTypes 実行種別配列
     * @param executorKey 実行キー
     * @return スケジュールリスト
     * @exception ScheduleManageException スケジュールの検索に失敗した場合
     */
    protected List<Schedule> findSchedules(
        Date from,
        Date to,
        int[] states,
        String masterId,
        String[] executorTypes,
        String executorKey
    ) throws ScheduleManageException{
        if(from != null && to != null){
            if(from.after(to)){
                throw new ScheduleManageException(
                    "from > to. from=" + from + ", to=" + to
                );
            }
        }
        if(scheduleList.size() == 0){
            return new ArrayList<Schedule>();
        }
        final DefaultSchedule searchKey =  new DefaultSchedule();
        List<Schedule> schedules = null;
        synchronized(scheduleList){
            int fromIndex = 0;
            if(from != null){
                searchKey.setTime(from);
                fromIndex = Math.abs(
                    Collections.binarySearch(scheduleList, searchKey) + 1
                );
                if(fromIndex != 0){
                    final Schedule schedule
                        = scheduleList.get(fromIndex - 1);
                    if(from.equals(schedule.getTime())){
                        fromIndex = fromIndex - 1;
                    }
                }
                if(fromIndex == scheduleList.size()){
                    return new ArrayList<Schedule>();
                }
                for(int i = fromIndex; --i >= 0;){
                    Schedule cmp = scheduleList.get(i);
                    if(from.equals(cmp.getTime())){
                        fromIndex = i;
                    }else{
                        break;
                    }
                }
            }
            int toIndex = scheduleList.size();
            if(to != null){
                searchKey.setTime(to);
                toIndex = Math.abs(
                    Collections.binarySearch(scheduleList, searchKey) + 1
                );
                if(toIndex == 0){
                    return new ArrayList<Schedule>();
                }
                for(int i = toIndex, imax = scheduleList.size(); i < imax; i++){
                    Schedule cmp = scheduleList.get(i);
                    if(to.equals(cmp.getTime())){
                        toIndex = i + 1;
                    }else{
                        break;
                    }
                }
            }
            schedules = new ArrayList<Schedule>(
                scheduleList.subList(fromIndex, toIndex)
            );
        }
        final Iterator<Schedule> itr = schedules.iterator();
        while(itr.hasNext()){
            final Schedule schedule = itr.next();
            if(masterId != null
                && !masterId.equals(schedule.getMasterId())){
                itr.remove();
                continue;
            }
            if(executorKey != null
                && schedule.getExecutorKey() != null
                && !executorKey.equals(schedule.getExecutorKey())){
                itr.remove();
                continue;
            }
            boolean isMatch = false;
            if(schedule.getExecutorType() != null
                && executorTypes != null && executorTypes.length != 0){
                for(int j = 0; j < executorTypes.length; j++){
                    if(schedule.getExecutorType().equals(executorTypes[j])){
                        isMatch = true;
                        break;
                    }
                }
                if(!isMatch){
                    itr.remove();
                    continue;
                }
            }
            if(states != null && states.length != 0){
                isMatch = false;
                for(int j = 0; j < states.length; j++){
                    if(schedule.getState() == states[j]){
                        isMatch = true;
                        break;
                    }
                }
                if(!isMatch){
                    itr.remove();
                }
            }
        }
        return schedules;
    }
    
    // ScheduleManagerのJavaDoc
    public List<Schedule> findExecutableSchedules(Date date, String[] executorTypes)
     throws ScheduleManageException{
        return findExecutableSchedules(date, executorTypes, null);
    }
    
    // ScheduleManagerのJavaDoc
    public List<Schedule> findExecutableSchedules(Date date, String[] executorTypes, String executorKey)
     throws ScheduleManageException{
        final List<Schedule> schedules = findSchedules(
            null,
            date,
            new int[]{Schedule.STATE_INITIAL, Schedule.STATE_RETRY},
            null,
            executorTypes,
            executorKey
        );
        if(schedules == null || schedules.size() == 0){
            return schedules;
        }
        final List<Schedule> result = new ArrayList<Schedule>();
        for(int i = 0, imax = schedules.size(); i < imax; i++){
            final Schedule schedule = schedules.get(i);
            final String[] depends = schedule.getDepends();
            if(depends == null){
                result.add(schedule);
            }else{
                boolean isClear = true;
                for(int j = 0; j < depends.length; j++){
                    final Set<Schedule> scheduleSet
                        = scheduleMasterMap.get(depends[j]);
                    if(scheduleSet == null){
                        continue;
                    }
                    Schedule[] dependsSchedules = null;
                    synchronized(scheduleSet){
                        dependsSchedules = scheduleSet.toArray(
                            new Schedule[scheduleSet.size()]
                        );
                    }
                    for(int k = 0; k < dependsSchedules.length; k++){
                        if(dependsSchedules[k].getInitialTime()
                                .after(schedule.getInitialTime())){
                            continue;
                        }
                        if(dependsSchedules[k].getState() != Schedule.STATE_END){
                            isClear = false;
                            break;
                        }
                    }
                    if(!isClear){
                        break;
                    }
                }
                if(isClear){
                    result.add(schedule);
                }
            }
        }
        return result;
    }
    
    public List<Schedule> findDependedSchedules(String id) throws ScheduleManageException{
        Schedule schedule = findSchedule(id);
        List<Schedule> result = new ArrayList<Schedule>();
        if(schedule == null){
            return result;
        }
        Set<String> depended = scheduleDependedMap.get(schedule.getMasterId());
        if(depended == null || depended.size() == 0){
            return result;
        }
        String[] ids = null;
        synchronized(depended){
            ids = depended.toArray(new String[depended.size()]);
        }
        for(String dependesId : ids){
            schedule = findSchedule(dependesId);
            if(schedule != null){
                result.add(schedule);
            }
        }
        Collections.sort(result);
        return result;
    }
    
    // ScheduleManagerのJavaDoc
    public void addSchedule(Schedule schedule) throws ScheduleManageException{
        addSchedule(schedule, true, true);
    }
    
    // DefaultScheduleManagerServiceMBeanのJavaDoc
    public void addSchedule(
        String masterId,
        Date time,
        String taskName,
        Object input,
        String[] depends,
        String executorKey,
        String executorType,
        long retryInterval,
        Date retryEndTime,
        long maxDelayTime
    ) throws ScheduleManageException{
        addSchedule(
            new DefaultSchedule(
                masterId,
                time,
                taskName,
                input,
                depends,
                executorKey,
                executorType,
                retryInterval,
                retryEndTime,
                maxDelayTime
            )
        );
    }
    
    /**
     * スケジュールを追加する。<p>
     *
     * @param schedule スケジュール
     * @param isCreateId スケジュールIDを再発番するかどうか
     * @param persist 永続化するかどうか
     * @exception ScheduleManageException スケジュールの追加に失敗した場合
     */
    protected void addSchedule(Schedule schedule, boolean isCreateId, boolean persist)
     throws ScheduleManageException{
        final Date standardDate = getStandardTimeDate(schedule.getTime());
        if(isCreateId){
            schedule.setId(createScheduleId());
        }
        
        synchronized(scheduleDateMap){
            List<Schedule> schedules = scheduleDateMap.get(standardDate);
            if(schedules == null){
                schedules = Collections.synchronizedList(new ArrayList<Schedule>());
                schedules.add(schedule);
                scheduleDateMap.put(standardDate, schedules);
            }else{
                synchronized(schedules){
                    schedules.add(schedule);
                    Collections.sort(schedules);
                }
            }
        }
        scheduleMap.put(schedule.getId(), schedule);
        synchronized(scheduleMasterMap){
            Set<Schedule> scheduleSet = scheduleMasterMap.get(schedule.getMasterId());
            if(scheduleSet == null){
                scheduleSet = Collections.synchronizedSet(new HashSet<Schedule>());
                scheduleSet.add(schedule);
                scheduleMasterMap.put(schedule.getMasterId(), scheduleSet);
            }else{
                synchronized(scheduleSet){
                    scheduleSet.add(schedule);
                }
            }
        }
        final String[] depends = schedule.getDepends();
        if(depends != null){
            synchronized(scheduleDependedMap){
                for(String id : depends){
                    Set<String> depended = scheduleDependedMap.get(id);
                    if(depended == null){
                        depended = Collections.synchronizedSet(new HashSet<String>());
                        scheduleDependedMap.put(id, depended);
                    }
                    depended.add(schedule.getId());
                }
            }
        }
        synchronized(scheduleList){
            scheduleList.add(schedule);
            Collections.sort(scheduleList);
        }
        
        if(persist){
            try{
                persistSchedule(schedule);
            }catch(IOException e){
                removeSchedule(schedule.getId());
                throw new ScheduleManageException(e);
            }
        }
    }
    
    /**
     * スケジュールを永続化する。<p>
     *
     * @param schedule スケジュール
     * @exception IOException スケジュールの追加に失敗した場合
     */
    protected void persistSchedule(Schedule schedule)
     throws IOException{
        if(persistDir == null){
            return;
        }
        final File baseDir = new File(persistDir);
        if(!baseDir.exists()){
            synchronized(persistDir){
                if(!baseDir.exists()){
                    if(!baseDir.mkdirs()){
                        throw new IOException("PersistDir can't make." + baseDir.getAbsolutePath());
                    }
                }
            }
        }
        
        final Date standardDate = getStandardTimeDate(schedule.getTime());
        final SimpleDateFormat format = new SimpleDateFormat(DATE_DIR_FORMAT);
        final File dateDir = new File(baseDir, format.format(standardDate));
        if(!dateDir.exists()){
            synchronized(persistDir){
                if(!dateDir.exists()){
                    if(!dateDir.mkdir()){
                        throw new IOException("Date Directory can't make." + dateDir.getAbsolutePath());
                    }
                }
            }
        }
        
        final File scheduleFile = new File(dateDir, schedule.getId());
        synchronized(schedule){
            CSVWriter writer = null;
            try{
                writer = new CSVWriter(new FileWriter(scheduleFile));
                writer.writeElement(schedule.getId());
                writer.writeElement(schedule.getMasterId());
                format.applyPattern(DATE_CSV_FORMAT);
                writer.writeElement(format.format(schedule.getTime()));
                writer.writeElement(schedule.getTaskName());
                if(schedule.getInput() == null){
                    writer.writeElement("");
                }else{
                    final StringBuilder buf = new StringBuilder();
                    final PropertyEditor editor
                        = NimbusPropertyEditorManager.findEditor(
                            schedule.getInput().getClass()
                        );
                    buf.append(schedule.getInput().getClass().getName());
                    buf.append(':');
                    if(editor == null){
                        buf.append(schedule.getInput());
                    }else{
                        editor.setValue(schedule.getInput());
                        buf.append(editor.getAsText());
                    }
                    writer.writeElement(buf.toString());
                }
                if(schedule.getDepends() == null){
                    writer.writeElement("");
                }else{
                    final StringArrayEditor editor = new StringArrayEditor();
                    editor.setValue(schedule.getDepends());
                    writer.writeElement(editor.getAsText());
                }
                if(schedule.getOutput() == null){
                    writer.writeElement("");
                }else{
                    final StringBuilder buf = new StringBuilder();
                    final PropertyEditor editor
                        = NimbusPropertyEditorManager.findEditor(
                            schedule.getOutput().getClass()
                        );
                    buf.append(schedule.getOutput().getClass().getName());
                    buf.append(':');
                    if(editor == null){
                        buf.append(schedule.getOutput());
                    }else{
                        editor.setValue(schedule.getOutput());
                        buf.append(editor.getAsText());
                    }
                    writer.writeElement(buf.toString());
                }
                writer.writeElement(schedule.getRetryInterval());
                if(schedule.getRetryEndTime() == null){
                    writer.writeElement("");
                }else{
                    writer.writeElement(format.format(schedule.getRetryEndTime()));
                }
                writer.writeElement(schedule.getMaxDelayTime());
                writer.writeElement(schedule.isRetry());
                writer.writeElement(schedule.getState());
                writer.writeElement(schedule.getControlState());
                writer.writeElement(schedule.getCheckState());
                if(schedule.getExecutorKey() == null){
                    writer.writeElement("");
                }else{
                    writer.writeElement(schedule.getExecutorKey());
                }
                if(schedule.getExecutorType() == null){
                    writer.writeElement("");
                }else{
                    writer.writeElement(schedule.getExecutorType());
                }
                if(schedule.getExecuteStartTime() == null){
                    writer.writeElement("");
                }else{
                    writer.writeElement(format.format(schedule.getExecuteStartTime()));
                }
                if(schedule.getExecuteEndTime() == null){
                    writer.writeElement("");
                }else{
                    writer.writeElement(format.format(schedule.getExecuteEndTime()));
                }
            }finally{
                if(writer != null){
                    writer.close();
                }
            }
        }
    }
    
    /**
     * 永続化されたローカル通番を読み込む。<p>
     *
     * @exception IOException ローカル通番の読み込みに失敗した場合
     */
    protected void loadLocalSequenceNumber() throws IOException{
        if(persistDir == null){
            return;
        }
        final File baseDir = new File(persistDir);
        if(!baseDir.exists()){
            return;
        }
        final File file = new File(baseDir, LOCAL_SEQUENCE_NUMBER_FILE);
        if(!file.exists()){
            return;
        }
        BufferedReader reader = null;
        try{
            reader = new BufferedReader(new FileReader(file));
            final String line = reader.readLine();
            if(line != null){
                sequenceNumber = Long.parseLong(line);
            }
        }finally{
            if(reader != null){
                reader.close();
            }
        }
    }
    
    /**
     * 永続化されたスケジュールを読み込む。<p>
     *
     * @exception IOException スケジュールの読み込みに失敗した場合
     */
    protected void loadSchedules() throws Exception{
        if(persistDir == null){
            return;
        }
        final File baseDir = new File(persistDir);
        if(!baseDir.exists()){
            return;
        }
        final File[] dateDirs = baseDir.listFiles();
        if(dateDirs == null || dateDirs.length == 0){
            return;
        }
        final SimpleDateFormat dateDirFormat
            = new SimpleDateFormat(DATE_DIR_FORMAT);
        final SimpleDateFormat dateCsvFormat
            = new SimpleDateFormat(DATE_CSV_FORMAT);
        for(int i = 0; i < dateDirs.length; i++){
            if(!dateDirs[i].isDirectory()){
                continue;
            }
            try{
                dateDirFormat.parse(dateDirs[i].getName());
            }catch(ParseException e){
                continue;
            }
            final File[] scheduleFiles = dateDirs[i].listFiles();
            if(scheduleFiles == null || scheduleFiles.length == 0){
                continue;
            }
            List<String> csv = null;
            for(int j = 0; j < scheduleFiles.length; j++){
                CSVReader reader = null;
                try{
                    reader = new CSVReader(new FileReader(scheduleFiles[j]));
                    csv = reader.readCSVLineList(csv);
                    final DefaultSchedule schedule = new DefaultSchedule();
                    schedule.setId(csv.get(0));
                    schedule.setMasterId(csv.get(1));
                    schedule.setTime(dateCsvFormat.parse(csv.get(2)));
                    schedule.setTaskName(csv.get(3));
                    final String inputStr = csv.get(4);
                    if(inputStr.length() == 0){
                        schedule.setInput(null);
                    }else{
                        final int index = inputStr.indexOf(':');
                        final Class<?> clazz = Utility.convertStringToClass(
                            inputStr.substring(0, index)
                        );
                        final PropertyEditor editor
                            = NimbusPropertyEditorManager.findEditor(clazz);
                        if(index == inputStr.length() - 1){
                            schedule.setInput(null);
                        }else{
                            editor.setAsText(
                                inputStr.substring(index + 1)
                            );
                            schedule.setInput(editor.getValue());
                        }
                    }
                    final String dependsStr = csv.get(5);
                    if(dependsStr.length() == 0){
                        schedule.setDepends(null);
                    }else{
                        final StringArrayEditor editor = new StringArrayEditor();
                        editor.setAsText(dependsStr);
                        schedule.setDepends((String[])editor.getValue());
                    }
                    final String outputStr = csv.get(6);
                    if(outputStr.length() == 0){
                        schedule.setOutput(null);
                    }else{
                        final int index = outputStr.indexOf(':');
                        Class<?> clazz = null;
                        try{
                            clazz = Utility.convertStringToClass(
                                outputStr.substring(0, index)
                            );
                            final PropertyEditor editor
                                = NimbusPropertyEditorManager.findEditor(clazz);
                            if(index == outputStr.length() - 1 || editor == null){
                                schedule.setOutput(outputStr);
                            }else{
                                editor.setAsText(
                                    outputStr.substring(index + 1)
                                );
                                schedule.setOutput(editor.getValue());
                            }
                        }catch(ClassNotFoundException e){
                            schedule.setOutput(outputStr);
                        }
                    }
                    schedule.setRetryInterval(
                        Long.parseLong(csv.get(7))
                    );
                    final String retryEndTimeStr = csv.get(8);
                    if(retryEndTimeStr != null
                         && retryEndTimeStr.length() != 0){
                        schedule.setRetryEndTime(
                            dateCsvFormat.parse(retryEndTimeStr)
                        );
                    }
                    schedule.setMaxDelayTime(
                        Long.parseLong(csv.get(9))
                    );
                    schedule.setRetry(
                        Boolean.valueOf(csv.get(10)).booleanValue()
                    );
                    schedule.setState(Integer.parseInt(csv.get(11)));
                    schedule.setControlState(
                        Integer.parseInt(csv.get(12))
                    );
                    schedule.setCheckState(
                        Integer.parseInt(csv.get(13))
                    );
                    final String executorKey = csv.get(14);
                    if(executorKey != null && executorKey.length() == 0){
                        schedule.setExecutorKey(null);
                    }else{
                        schedule.setExecutorKey(executorKey);
                    }
                    final String executorType = csv.get(15);
                    if(executorType != null && executorType.length() == 0){
                        schedule.setExecutorType(null);
                    }else{
                        schedule.setExecutorType(executorType);
                    }
                    final String executeStartTimeStr = csv.get(16);
                    if(executeStartTimeStr != null
                         && executeStartTimeStr.length() != 0){
                        schedule.setExecuteStartTime(
                            dateCsvFormat.parse(executeStartTimeStr)
                        );
                    }
                    final String executeEndTimeStr = csv.get(17);
                    if(executeEndTimeStr != null
                         && executeEndTimeStr.length() != 0){
                        schedule.setExecuteEndTime(
                            dateCsvFormat.parse(executeEndTimeStr)
                        );
                    }
                    addSchedule(schedule, false, false);
                }finally{
                    if(reader != null){
                        reader.close();
                    }
                }
            }
        }
    }
    
    /**
     * 永続化されたスケジュールを削除する。<p>
     *
     * @param schedule スケジュール
     * @exception IOException スケジュールの削除に失敗した場合
     */
    protected void removePersistSchedule(Schedule schedule)
     throws IOException{
        if(persistDir == null){
            return;
        }
        final File baseDir = new File(persistDir);
        if(!baseDir.exists()){
            return;
        }
        
        final Date standardDate = getStandardTimeDate(schedule.getTime());
        final SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
        final File dateDir = new File(baseDir, format.format(standardDate));
        if(!dateDir.exists()){
            return;
        }
        
        final File scheduleFile = new File(dateDir, schedule.getId());
        synchronized(schedule){
            if(scheduleFile.exists()){
                if(!scheduleFile.delete()){
                    throw new IOException("Persist file can't delete." + scheduleFile.getAbsolutePath());
                }
            }
        }
        final File[] scheduleFiles = dateDir.listFiles();
        if(scheduleFiles == null || scheduleFiles.length == 0){
            dateDir.delete();
        }
    }
    
    // ScheduleManagerのJavaDoc
    public boolean reschedule(String id, Date time)
     throws ScheduleManageException{
        final Schedule schedule = scheduleMap.get(id);
        if(schedule == null){
            return false;
        }
        if(!removeSchedule(id)){
            return false;
        }
        schedule.setTime(time);
        schedule.setCheckState(Schedule.CHECK_STATE_INITIAL);
        addSchedule(schedule, false, true);
        return true;
    }
    
    // ScheduleManagerのJavaDoc
    public boolean removeSchedule(String id) throws ScheduleManageException{
        final Schedule schedule = scheduleMap.get(id);
        if(schedule == null){
            return false;
        }
        try{
            removePersistSchedule(schedule);
        }catch(IOException e){
            throw new ScheduleManageException(e);
        }
        scheduleMap.remove(schedule.getId());
        synchronized(scheduleList){
            scheduleList.remove(schedule);
        }
        synchronized(scheduleMasterMap){
            Set<Schedule> scheduleSet = scheduleMasterMap.get(schedule.getMasterId());
            if(scheduleSet != null){
                synchronized(scheduleSet){
                    scheduleSet.remove(schedule);
                }
                if(scheduleSet.size() == 0){
                    scheduleMasterMap.remove(schedule.getMasterId());
                }
            }
        }
        synchronized(scheduleDependedMap){
            final Iterator<Set<String>> dependedItr = scheduleDependedMap.values().iterator();
            while(dependedItr.hasNext()){
                Set<String> depended = dependedItr.next();
                depended.remove(schedule.getId());
            }
        }
        final Date standardDate = getStandardTimeDate(schedule.getTime());
        synchronized(scheduleDateMap){
            List<Schedule> schedules = scheduleDateMap.get(standardDate);
            if(schedules != null){
                synchronized(schedules){
                    schedules.remove(schedule);
                }
                if(schedules.size() == 0){
                    scheduleDateMap.remove(standardDate);
                }
            }
        }
        return true;
    }
    
    // ScheduleManagerのJavaDoc
    public boolean removeScheduleByMasterId(String masterId) throws ScheduleManageException{
        final Set<Schedule> scheduleSet = scheduleMasterMap.get(masterId);
        if(scheduleSet == null){
            return false;
        }
        Schedule[] schedules = null;
        synchronized(scheduleSet){
            schedules = scheduleSet.toArray(
                new Schedule[scheduleSet.size()]
            );
        }
        boolean isRemove = false;
        for(int i = 0; i < schedules.length; i++){
            isRemove |= removeSchedule(schedules[i].getId());
        }
        return isRemove;
    }
    
    // ScheduleManagerのJavaDoc
    public boolean removeSchedule(Date date) throws ScheduleManageException{
        final Date standardDate = getStandardTimeDate(date);
        Schedule[] schedules = null;
        synchronized(scheduleDateMap){
            List<Schedule> scheduleList = scheduleDateMap.get(standardDate);
            if(scheduleList == null || scheduleList.size() == 0){
                return false;
            }
            synchronized(scheduleList){
                schedules = scheduleList.toArray(
                    new Schedule[scheduleList.size()]
                );
            }
        }
        boolean isRemove = false;
        for(int i = 0; i < schedules.length; i++){
            isRemove |= removeSchedule(schedules[i].getId());
        }
        return isRemove;
    }
    
    // ScheduleManagerのJavaDoc
    public boolean removeSchedule(Date from, Date to, int[] states, String masterId) throws ScheduleManageException{
        List<Schedule> schedules = findSchedules(from, to, states, masterId);
        boolean isRemove = false;
        for(int i = 0, imax = schedules.size(); i < imax; i++){
            isRemove |= removeSchedule(schedules.get(i).getId());
        }
        return isRemove;
    }
    
    // ScheduleManagerのJavaDoc
    public void setExecutorKey(String id, String key) throws ScheduleManageException{
        final Schedule schedule = scheduleMap.get(id);
        if(schedule == null){
            throw new ScheduleManageException("Schedule not found : " + id);
        }
        schedule.setExecutorKey(key);
        try{
            persistSchedule(schedule);
        }catch(IOException e){
            throw new ScheduleManageException(e);
        }
    }
    
    // ScheduleManagerのJavaDoc
    public int getState(String id) throws ScheduleStateControlException{
        final Schedule schedule = scheduleMap.get(id);
        if(schedule == null){
            throw new ScheduleStateControlException("Schedule not found : " + id);
        }
        return schedule.getState();
    }
    
    // ScheduleManagerのJavaDoc
    public int getControlState(String id) throws ScheduleStateControlException{
        final Schedule schedule = scheduleMap.get(id);
        if(schedule == null){
            throw new ScheduleStateControlException("Schedule not found : " + id);
        }
        return schedule.getControlState();
    }
    
    // ScheduleManagerのJavaDoc
    public boolean changeState(String id, int state) throws ScheduleStateControlException{
        return changeState(id, state, null);
    }
    
    // ScheduleManagerのJavaDoc
    public boolean changeState(String id, int oldState, int newState) throws ScheduleStateControlException{
        final Schedule schedule = scheduleMap.get(id);
        if(schedule == null){
            throw new ScheduleStateControlException("Schedule not found : " + id);
        }
        switch(newState){
        case Schedule.STATE_RUN:
            schedule.setExecuteStartTime(new Date());
            break;
        case Schedule.STATE_END:
        case Schedule.STATE_FAILED:
        case Schedule.STATE_ABORT:
            schedule.setExecuteEndTime(new Date());
            break;
        case Schedule.STATE_INITIAL:
        case Schedule.STATE_ENTRY:
            schedule.setExecuteStartTime(null);
            schedule.setExecuteEndTime(null);
            break;
        case Schedule.STATE_RETRY:
        case Schedule.STATE_PAUSE:
            break;
        default:
            throw new ScheduleStateControlException("Unknown state : " + newState);
        }
        if(oldState != schedule.getState()){
            return false;
        }
        final boolean isChange = schedule.getState() != newState;
        schedule.setState(newState);
        try{
            persistSchedule(schedule);
        }catch(IOException e){
            throw new ScheduleStateControlException(e);
        }
        return isChange;
    }
    
    // ScheduleManagerのJavaDoc
    public boolean changeState(String id, int state, Object output) throws ScheduleStateControlException{
        final Schedule schedule = scheduleMap.get(id);
        if(schedule == null){
            throw new ScheduleStateControlException("Schedule not found : " + id);
        }
        switch(state){
        case Schedule.STATE_RUN:
            schedule.setExecuteStartTime(new Date());
            break;
        case Schedule.STATE_END:
        case Schedule.STATE_FAILED:
        case Schedule.STATE_ABORT:
            schedule.setExecuteEndTime(new Date());
            break;
        case Schedule.STATE_INITIAL:
        case Schedule.STATE_ENTRY:
            schedule.setExecuteStartTime(null);
            schedule.setExecuteEndTime(null);
            break;
        case Schedule.STATE_RETRY:
        case Schedule.STATE_PAUSE:
            break;
        default:
            throw new ScheduleStateControlException("Unknown state : " + state);
        }
        schedule.setOutput(output);
        final boolean isChange = schedule.getState() != state;
        schedule.setState(state);
        try{
            persistSchedule(schedule);
        }catch(IOException e){
            throw new ScheduleStateControlException(e);
        }
        return isChange;
    }
    
    // ScheduleManagerのJavaDoc
    public boolean changeControlState(String id, int oldState, int newState) throws ScheduleStateControlException{
        final Schedule schedule = scheduleMap.get(id);
        if(schedule == null){
            throw new ScheduleStateControlException("Schedule not found : " + id);
        }
        switch(newState){
        case Schedule.CONTROL_STATE_PAUSE:
            if(getState(id) != Schedule.STATE_RUN){
                return false;
            }
            break;
        case Schedule.CONTROL_STATE_RESUME:
            if(getState(id) != Schedule.STATE_PAUSE){
                return false;
            }
            break;
        case Schedule.CONTROL_STATE_ABORT:
            if(getState(id) != Schedule.STATE_RUN){
                return false;
            }
            break;
        default:
            throw new ScheduleStateControlException("Unknown state : " + newState);
        }
        if(oldState != schedule.getControlState()){
            return false;
        }
        final boolean isChange = schedule.getControlState() != newState;
        if(!isChange){
            return false;
        }
        schedule.setControlState(newState);
        try{
            persistSchedule(schedule);
        }catch(IOException e){
            throw new ScheduleStateControlException(e);
        }
        try{
            if(scheduleControlListeners != null
                 && scheduleControlListeners.size() != 0){
                synchronized(scheduleControlListeners){
                    final Iterator<ScheduleControlListener> itr = scheduleControlListeners.iterator();
                    while(itr.hasNext()){
                        final ScheduleControlListener listener
                            = itr.next();
                        listener.changedControlState(id, newState);
                    }
                }
            }
        }catch(ScheduleStateControlException e){
            schedule.setControlState(Schedule.CONTROL_STATE_FAILED);
            throw e;
        }
        return isChange;
    }
    
    // ScheduleManagerのJavaDoc
    public boolean changeControlState(String id, int state) throws ScheduleStateControlException{
        final Schedule schedule = scheduleMap.get(id);
        if(schedule == null){
            throw new ScheduleStateControlException("Schedule not found : " + id);
        }
        switch(state){
        case Schedule.CONTROL_STATE_PAUSE:
            if(getState(id) != Schedule.STATE_RUN){
                return false;
            }
            break;
        case Schedule.CONTROL_STATE_RESUME:
            if(getState(id) != Schedule.STATE_PAUSE){
                return false;
            }
            break;
        case Schedule.CONTROL_STATE_ABORT:
            if(getState(id) != Schedule.STATE_RUN){
                return false;
            }
            break;
        default:
            throw new ScheduleStateControlException("Unknown state : " + state);
        }
        final boolean isChange = schedule.getControlState() != state;
        if(!isChange){
            return false;
        }
        try{
            persistSchedule(schedule);
        }catch(IOException e){
            throw new ScheduleStateControlException(e);
        }
        schedule.setControlState(state);
        try{
            if(scheduleControlListeners != null
                 && scheduleControlListeners.size() != 0){
                synchronized(scheduleControlListeners){
                    final Iterator<ScheduleControlListener> itr = scheduleControlListeners.iterator();
                    while(itr.hasNext()){
                        final ScheduleControlListener listener
                            = itr.next();
                        listener.changedControlState(id, state);
                    }
                }
            }
        }catch(ScheduleStateControlException e){
            schedule.setControlState(Schedule.CONTROL_STATE_FAILED);
            throw e;
        }
        return isChange;
    }
    
    // ScheduleManagerのJavaDoc
    public void addScheduleControlListener(ScheduleControlListener listener){
        scheduleControlListeners.add(listener);
    }
    
    // ScheduleManagerのJavaDoc
    public void removeScheduleControlListener(ScheduleControlListener listener){
        scheduleControlListeners.remove(listener);
    }
    
    /**
     * タイムオーバー監視。<p>
     *
     * @author M.Takata
     */
    protected class TimeoverChecker implements DaemonRunnable<Object>{
        
        /**
         * デーモンが開始した時に呼び出される。<p>
         * 
         * @return 常にtrueを返す
         */
        public boolean onStart() {
            return true;
        }
        
        /**
         * デーモンが停止した時に呼び出される。<p>
         * 
         * @return 常にtrueを返す
         */
        public boolean onStop() {
            return true;
        }
        
        /**
         * デーモンが中断した時に呼び出される。<p>
         * 
         * @return 常にtrueを返す
         */
        public boolean onSuspend() {
            return true;
        }
        
        /**
         * デーモンが再開した時に呼び出される。<p>
         * 
         * @return 常にtrueを返す
         */
        public boolean onResume() {
            return true;
        }
        
        /**
         * 一定時間空ける。<p>
         * 
         * @param ctrl DaemonControlオブジェクト
         * @return スケジュールの配列
         */
        public Object provide(DaemonControl ctrl) throws Throwable{
            Thread.sleep(getTimeoverCheckInterval());
            return null;
        }
        
        /**
         * 最大遅延時間をチェックする。<p>
         *
         * @param input null
         * @param ctrl DaemonControlオブジェクト
         */
        public void consume(Object input, DaemonControl ctrl)
         throws Throwable{
            
            final Calendar nowCal = Calendar.getInstance();
            List<Schedule> schedules = findSchedules(nowCal.getTime(), (Date)null);
            Calendar tmpCal = Calendar.getInstance();
            for(int i = 0, imax = schedules.size(); i < imax; i++){
                Schedule schedule = schedules.get(i);
                tmpCal.clear();
                tmpCal.setTime(schedule.getTime());
                if(schedule.getCheckState() != Schedule.CHECK_STATE_TIMEOVER
                     && tmpCal.before(nowCal)
                     && schedule.getMaxDelayTime() > 0
                     && schedule.getState() != Schedule.STATE_END
                     && schedule.getState() != Schedule.STATE_FAILED
                     && schedule.getState() != Schedule.STATE_ABORT
                ){
                    tmpCal.clear();
                    tmpCal.setTimeInMillis(
                        schedule.getTime().getTime()
                            + schedule.getMaxDelayTime()
                    );
                    if(tmpCal.after(nowCal) || tmpCal.equals(nowCal)){
                        continue;
                    }
                    schedule.setCheckState(Schedule.CHECK_STATE_TIMEOVER);
                    getLogger().write(
                        MSG_ID_TIMEOVER_ERROR,
                        schedule.getId(),
                        new Integer(schedule.getState())
                    );
                }
            }
        }
        
        /**
         * 何もしない。<p>
         */
        public void garbage(){
        }
    }
}