001/*
002 * Copyright 2007 The Kuali Foundation
003 * 
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 * http://www.opensource.org/licenses/ecl2.php
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.ole.sys.batch.service.impl;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Calendar;
021import java.util.Collections;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Set;
028
029import org.apache.commons.lang.StringUtils;
030import org.apache.log4j.Logger;
031import org.kuali.ole.sys.OLEConstants;
032import org.kuali.ole.sys.batch.BatchJobStatus;
033import org.kuali.ole.sys.batch.BatchSpringContext;
034import org.kuali.ole.sys.batch.Job;
035import org.kuali.ole.sys.batch.JobDescriptor;
036import org.kuali.ole.sys.batch.JobListener;
037import org.kuali.ole.sys.batch.ScheduleStep;
038import org.kuali.ole.sys.batch.SimpleTriggerDescriptor;
039import org.kuali.ole.sys.batch.Step;
040import org.kuali.ole.sys.batch.service.SchedulerService;
041import org.kuali.ole.sys.context.SpringContext;
042import org.kuali.ole.sys.service.BatchModuleService;
043import org.kuali.ole.sys.service.impl.OleModuleServiceImpl;
044import org.kuali.rice.core.api.datetime.DateTimeService;
045import org.kuali.rice.coreservice.framework.parameter.ParameterService;
046import org.kuali.rice.krad.service.KualiModuleService;
047import org.kuali.rice.krad.service.MailService;
048import org.kuali.rice.krad.service.ModuleService;
049import org.quartz.JobDetail;
050import org.quartz.JobExecutionContext;
051import org.quartz.ObjectAlreadyExistsException;
052import org.quartz.Scheduler;
053import org.quartz.SchedulerException;
054import org.quartz.Trigger;
055import org.quartz.UnableToInterruptJobException;
056import org.springframework.beans.factory.NoSuchBeanDefinitionException;
057import org.springframework.transaction.annotation.Transactional;
058
059@Transactional
060public class SchedulerServiceImpl implements SchedulerService {
061    private static final Logger LOG = Logger.getLogger(SchedulerServiceImpl.class);
062    protected static final String SOFT_DEPENDENCY_CODE = "softDependency";
063    protected static final String HARD_DEPENDENCY_CODE = "hardDependency";
064
065    protected Scheduler scheduler;
066    protected JobListener jobListener;
067    private KualiModuleService kualiModuleService;
068    protected ParameterService parameterService;
069    protected DateTimeService dateTimeService;
070    private MailService mailService;
071    /**
072     * Holds a list of job name to job descriptor mappings for those jobs that are externalized (i.e. the module service is responsible for reporting their status)
073     */
074    protected Map<String, JobDescriptor> externalizedJobDescriptors;
075
076    protected static final List<String> jobStatuses = new ArrayList<String>();
077
078    static {
079        jobStatuses.add(SCHEDULED_JOB_STATUS_CODE);
080        jobStatuses.add(SUCCEEDED_JOB_STATUS_CODE);
081        jobStatuses.add(CANCELLED_JOB_STATUS_CODE);
082        jobStatuses.add(RUNNING_JOB_STATUS_CODE);
083        jobStatuses.add(FAILED_JOB_STATUS_CODE);
084    }
085
086    public SchedulerServiceImpl() {
087        externalizedJobDescriptors = new HashMap<String, JobDescriptor>();
088    }
089    
090    /**
091     * @see org.kuali.ole.sys.batch.service.SchedulerService#initialize()
092     */
093    @Override
094    public void initialize() {
095        LOG.info("Initializing the schedule");
096        jobListener.setSchedulerService(this);
097        try {
098            scheduler.addGlobalJobListener(jobListener);
099        }
100        catch (SchedulerException e) {
101            throw new RuntimeException("SchedulerServiceImpl encountered an exception when trying to register the global job listener", e);
102        }
103        JobDescriptor jobDescriptor;
104        for (ModuleService moduleService : kualiModuleService.getInstalledModuleServices()) {
105            initializeJobsForModule(moduleService);
106            initializeTriggersForModule(moduleService);
107        }
108
109        dropDependenciesNotScheduled();
110    }
111
112    /**
113     * Initializes all of the jobs into Quartz for the given ModuleService
114     * @param moduleService the ModuleService implementation to initalize jobs for
115     */
116    protected void initializeJobsForModule(ModuleService moduleService) {
117        if ( LOG.isInfoEnabled() ) {
118            LOG.info("Loading scheduled jobs for: " + moduleService.getModuleConfiguration().getNamespaceCode());
119        }
120        JobDescriptor jobDescriptor;
121        if ( moduleService.getModuleConfiguration().getJobNames() != null ) { 
122            for (String jobName : moduleService.getModuleConfiguration().getJobNames()) {
123                try {
124                    if (moduleService instanceof BatchModuleService && ((BatchModuleService) moduleService).isExternalJob(jobName)) {
125                        jobDescriptor = new JobDescriptor();
126                        jobDescriptor.setBeanName(jobName);
127                        jobDescriptor.setGroup(SCHEDULED_GROUP);
128                        jobDescriptor.setDurable(false);
129                        externalizedJobDescriptors.put(jobName, jobDescriptor);
130                    }
131                    else {
132                        jobDescriptor = BatchSpringContext.getJobDescriptor(jobName);
133                    }
134                    jobDescriptor.setNamespaceCode(moduleService.getModuleConfiguration().getNamespaceCode());
135                    loadJob(jobDescriptor);
136                } catch (NoSuchBeanDefinitionException ex) {
137                    LOG.error("unable to find job bean definition for job: " + ex.getBeanName());
138                } catch ( Exception ex ) {
139                    LOG.error( "Unable to install " + jobName + " job into scheduler.", ex );
140                }
141            }
142        }
143    }
144
145    /**
146     * Loops through all the triggers associated with the given module service, adding each trigger to Quartz
147     * @param moduleService the ModuleService instance to initialize triggers for
148     */
149    protected void initializeTriggersForModule(ModuleService moduleService) {
150        if ( moduleService.getModuleConfiguration().getTriggerNames() != null ) {
151            for (String triggerName : moduleService.getModuleConfiguration().getTriggerNames()) {
152                try {
153                    addTrigger(BatchSpringContext.getTriggerDescriptor(triggerName).getTrigger());
154                } catch (NoSuchBeanDefinitionException ex) {
155                    LOG.error("unable to find trigger definition: " + ex.getBeanName());
156                } catch ( Exception ex ) {
157                    LOG.error( "Unable to install " + triggerName + " trigger into scheduler.", ex );
158                }
159            }
160        }
161    }
162
163
164    protected void loadJob(JobDescriptor jobDescriptor) {
165        JobDetail jobDetail = jobDescriptor.getJobDetail();
166        addJob(jobDetail);
167        if (SCHEDULED_GROUP.equals(jobDetail.getGroup())) {
168            jobDetail.setGroup(UNSCHEDULED_GROUP);
169            addJob(jobDetail);
170        }
171    }
172
173    /**
174     * Remove dependencies that are not scheduled. Important for modularization if some
175     * modules arn't loaded or if institutions don't schedule some dependencies
176     */
177    protected void dropDependenciesNotScheduled() {
178        try {
179            List<String> scheduledGroupJobNames = Arrays.asList(scheduler.getJobNames(SCHEDULED_GROUP));
180
181            for (String jobName : scheduledGroupJobNames) {
182                JobDescriptor jobDescriptor = BatchSpringContext.getJobDescriptor(jobName);
183
184                if (jobDescriptor != null && jobDescriptor.getDependencies() != null) {
185                    // dependenciesToBeRemoved so to avoid ConcurrentModificationException
186                    ArrayList<Entry<String, String>> dependenciesToBeRemoved = new ArrayList<Entry<String, String>>();
187                    Set<Entry<String, String>> dependenciesSet = jobDescriptor.getDependencies().entrySet();
188                    for (Entry<String, String> dependency : dependenciesSet) {
189                        String dependencyJobName = dependency.getKey();
190                        if (!scheduledGroupJobNames.contains(dependencyJobName)) {
191                            LOG.warn("Removing dependency " + dependencyJobName + " from " + jobName + " because it is not scheduled.");
192                            dependenciesToBeRemoved.add(dependency);
193                        }
194                    }
195                    dependenciesSet.removeAll(dependenciesToBeRemoved);
196                }
197            }
198        } catch (SchedulerException e) {
199            throw new RuntimeException("Caught exception while trying to drop dependencies that are not scheduled", e);
200        }
201    }
202
203    /**
204     * @see org.kuali.ole.sys.batch.service.SchedulerService#initializeJob(java.lang.String,org.kuali.ole.sys.batch.Job)
205     */
206    @Override
207    public void initializeJob(String jobName, Job job) {
208        job.setSchedulerService(this);
209        job.setParameterService(parameterService);
210        job.setSteps(BatchSpringContext.getJobDescriptor(jobName).getSteps());
211        job.setDateTimeService(dateTimeService);
212    }
213
214    /**
215     * @see org.kuali.ole.sys.batch.service.SchedulerService#hasIncompleteJob()
216     */
217    @Override
218    public boolean hasIncompleteJob() {
219        StringBuilder log = new StringBuilder("The schedule has incomplete jobs.");
220        boolean hasIncompleteJob = false;
221        for (String scheduledJobName : getJobNamesForScheduleJob() ) {
222            JobDetail scheduledJobDetail = getScheduledJobDetail(scheduledJobName);
223            
224            boolean jobIsIncomplete = isIncomplete(scheduledJobDetail);
225            if (jobIsIncomplete) {
226                log.append("\n\t").append(scheduledJobDetail.getFullName());
227                hasIncompleteJob = true;
228            }
229        }
230        if (hasIncompleteJob) {
231            LOG.info(log);
232        }
233        return hasIncompleteJob;
234    }
235
236    protected boolean isIncomplete(JobDetail scheduledJobDetail) {
237        if ( scheduledJobDetail == null ) {
238            return false;
239        }
240         
241        return !SCHEDULE_JOB_NAME.equals(scheduledJobDetail.getName()) && (isPending(scheduledJobDetail) || isScheduled(scheduledJobDetail));
242    }
243
244    /**
245     * @see org.kuali.ole.sys.batch.service.SchedulerService#isPastScheduleCutoffTime()
246     */
247    @Override
248    public boolean isPastScheduleCutoffTime() {
249        return isPastScheduleCutoffTime(dateTimeService.getCurrentCalendar(), true);
250    }
251
252    protected boolean isPastScheduleCutoffTime(Calendar dateTime, boolean log) {
253        try {
254            Date scheduleCutoffTimeTemp = scheduler.getTriggersOfJob(SCHEDULE_JOB_NAME, SCHEDULED_GROUP)[0].getPreviousFireTime();
255            Calendar scheduleCutoffTime;
256            if (scheduleCutoffTimeTemp == null) {
257                scheduleCutoffTime = dateTimeService.getCurrentCalendar();
258            }
259            else {
260                scheduleCutoffTime = dateTimeService.getCalendar(scheduleCutoffTimeTemp);
261            }
262            String cutoffParameter = parameterService.getParameterValueAsString(ScheduleStep.class, OLEConstants.SystemGroupParameterNames.BATCH_SCHEDULE_CUTOFF_TIME);
263            String[] scheduleStepCutoffTime = StringUtils.split(cutoffParameter, ":");
264            if ( scheduleStepCutoffTime.length != 3 && scheduleStepCutoffTime.length != 4 ) {
265                throw new IllegalArgumentException( "Error! The " + OLEConstants.SystemGroupParameterNames.BATCH_SCHEDULE_CUTOFF_TIME + " parameter had an invalid value: " + cutoffParameter );
266            }
267            // if there are 4 components, then we have an AM/PM delimiter
268            // otherwise, assume 24-hour time
269            if ( scheduleStepCutoffTime.length == 4 ) {
270                int hour = Integer.parseInt(scheduleStepCutoffTime[0]);
271                // need to adjust for meaning of hour
272                if ( hour == 12 ) {
273                    hour = 0;
274                } else {
275                    hour--;
276            }
277                scheduleCutoffTime.set(Calendar.HOUR, hour );
278                if ( StringUtils.containsIgnoreCase(scheduleStepCutoffTime[3], "AM") ) {
279                    scheduleCutoffTime.set(Calendar.AM_PM, Calendar.AM);
280                } else {
281                scheduleCutoffTime.set(Calendar.AM_PM, Calendar.PM);
282            }
283            } else {
284                scheduleCutoffTime.set(Calendar.HOUR_OF_DAY, Integer.parseInt(scheduleStepCutoffTime[0]));
285            }
286            scheduleCutoffTime.set(Calendar.MINUTE, Integer.parseInt(scheduleStepCutoffTime[1]));
287            scheduleCutoffTime.set(Calendar.SECOND, Integer.parseInt(scheduleStepCutoffTime[2]));
288            if (parameterService.getParameterValueAsBoolean(ScheduleStep.class, OLEConstants.SystemGroupParameterNames.BATCH_SCHEDULE_CUTOFF_TIME_IS_NEXT_DAY)) {
289                scheduleCutoffTime.add(Calendar.DAY_OF_YEAR, 1);
290            }
291            boolean isPastScheduleCutoffTime = dateTime.after(scheduleCutoffTime);
292            if (log) {
293                LOG.info(new StringBuilder("isPastScheduleCutoffTime=").append(isPastScheduleCutoffTime).append(" : ").append(dateTimeService.toDateTimeString(dateTime.getTime())).append(" / ").append(dateTimeService.toDateTimeString(scheduleCutoffTime.getTime())));
294            }
295            return isPastScheduleCutoffTime;
296        }
297        catch (NumberFormatException e) {
298            throw new RuntimeException("Caught exception while checking whether we've exceeded the schedule cutoff time", e);
299        }
300        catch (SchedulerException e) {
301            throw new RuntimeException("Caught exception while checking whether we've exceeded the schedule cutoff time", e);
302        }
303    }
304
305    /**
306     * @see org.kuali.ole.sys.batch.service.SchedulerService#processWaitingJobs()
307     */
308    @Override
309    public void processWaitingJobs() {
310        for ( String scheduledJobName : getJobNamesForScheduleJob() ) {
311            JobDetail jobDetail = getScheduledJobDetail(scheduledJobName);
312            if (isPending(jobDetail)) {
313                if (shouldScheduleJob(jobDetail)) {
314                    scheduleJob(SCHEDULED_GROUP, scheduledJobName, 0, 0, new Date(), null, Collections.singletonMap(Job.MASTER_JOB_NAME, SCHEDULE_JOB_NAME) );
315                }
316                if (shouldCancelJob(jobDetail)) {
317                    updateStatus(SCHEDULED_GROUP, scheduledJobName, CANCELLED_JOB_STATUS_CODE);
318                }
319            }
320        }
321    }
322
323    /**
324     * @see org.kuali.ole.sys.batch.service.SchedulerService#logScheduleResults()
325     */
326    @Override
327    public void logScheduleResults() {
328        StringBuilder scheduleResults = new StringBuilder("The schedule completed.");
329        for ( String scheduledJobName : getJobNamesForScheduleJob() ) {
330            JobDetail jobDetail = getScheduledJobDetail(scheduledJobName);
331            if ( jobDetail != null &&  !SCHEDULE_JOB_NAME.equals(jobDetail.getName())) {
332                scheduleResults.append("\n\t").append(jobDetail.getName()).append("=").append(getStatus(jobDetail));
333            }
334        }
335        LOG.info(scheduleResults);
336    }
337
338    /**
339     * @see org.kuali.ole.sys.batch.service.SchedulerService#shouldNotRun(org.quartz.JobDetail)
340     */
341    @Override
342    public boolean shouldNotRun(JobDetail jobDetail) {
343        if (SCHEDULED_GROUP.equals(jobDetail.getGroup())) {
344            if (isCancelled(jobDetail)) {
345                if ( LOG.isInfoEnabled() ) {
346                    LOG.info("Telling listener not to run job, because it has been cancelled: " + jobDetail.getName());
347                }
348                return true;
349            }
350            else {
351                for (String dependencyJobName : getJobDependencies(jobDetail.getName()).keySet()) {
352                    if (!isDependencySatisfiedPositively(jobDetail, getScheduledJobDetail(dependencyJobName))) {
353                        if ( LOG.isInfoEnabled() ) {
354                            LOG.info("Telling listener not to run job, because a dependency has not been satisfied positively: "+jobDetail.getName()+" (dependency job = "+dependencyJobName+")");
355                        }
356                        return true;
357                    }
358                }
359            }
360        }
361        return false;
362    }
363
364    /**
365     * @see org.kuali.ole.sys.batch.service.SchedulerService#updateStatus(org.quartz.JobDetail,java.lang.String jobStatus)
366     */
367    @Override
368    public void updateStatus(JobDetail jobDetail, String jobStatus) {
369        if ( LOG.isInfoEnabled() ) {
370            LOG.info("Updating status of job: "+jobDetail.getName()+"="+jobStatus);
371        }
372        jobDetail.getJobDataMap().put(JOB_STATUS_PARAMETER, jobStatus);
373    }
374
375    @Override
376    public void runJob(String jobName, String requestorEmailAddress) {
377        runJob(jobName, 0, 0, new Date(), requestorEmailAddress);
378    }
379
380    @Override
381    public void runJob(String jobName, int startStep, int stopStep, Date startTime, String requestorEmailAddress) {
382        runJob(UNSCHEDULED_GROUP, jobName, startStep, stopStep, startTime, requestorEmailAddress);
383    }
384
385    @Override
386    public void runJob(String groupName, String jobName, int startStep, int stopStep, Date jobStartTime, String requestorEmailAddress) {
387        if ( LOG.isInfoEnabled() ) {
388            LOG.info("Executing user initiated job: " + groupName + "." + jobName + " (startStep=" + startStep + " / stopStep=" + stopStep + " / startTime=" + jobStartTime + " / requestorEmailAddress=" + requestorEmailAddress + ")");
389        }
390
391        try {
392            JobDetail jobDetail = scheduler.getJobDetail(jobName, groupName);
393            scheduleJob(groupName, jobName, startStep, stopStep, jobStartTime, requestorEmailAddress, null);
394        }
395        catch (SchedulerException ex) {
396            throw new RuntimeException("Unable to run a job directly", ex);
397        }
398    }
399
400    public void runStep(String groupName, String jobName, String stepName, Date startTime, String requestorEmailAddress) {
401        if ( LOG.isInfoEnabled() ) {
402            LOG.info("Executing user initiated step: " + stepName + " / requestorEmailAddress=" + requestorEmailAddress);
403        }
404
405        // abort if the step is already running
406        if (isJobRunning(jobName)) {
407            LOG.warn("Attempt to run job already executing, aborting");
408            return;
409        }
410        int stepNum = 1;
411        boolean stepFound = false;
412        BatchJobStatus job = getJob(groupName, jobName);
413        for (Step step : job.getSteps()) {
414            if (step.getName().equals(stepName)) {
415                stepFound = true;
416                break;
417            }
418            stepNum++;
419        }
420        if (stepFound) {
421            runJob(groupName, jobName, stepNum, stepNum, startTime, requestorEmailAddress);
422        }
423        else {
424            LOG.warn("Unable to find step " + stepName + " in job " + groupName + "." + jobName);
425        }
426    }
427
428    @Override
429    public boolean isJobRunning(String jobName) {
430        List<JobExecutionContext> runningJobs = getRunningJobs();
431        for (JobExecutionContext jobCtx : runningJobs) {
432            if (jobCtx.getJobDetail().getName().equals(jobName)) {
433                return true;
434            }
435        }
436        return false;
437    }
438
439    protected void addJob(JobDetail jobDetail) {
440        try {
441            if ( LOG.isInfoEnabled() ) {
442                LOG.info("Adding job: " + jobDetail.getFullName());
443            }
444            scheduler.addJob(jobDetail, true);
445        }
446        catch (SchedulerException e) {
447            throw new RuntimeException("Caught exception while adding job: " + jobDetail.getFullName(), e);
448        }
449    }
450
451    protected void addTrigger(Trigger trigger) {
452        try {
453            if (UNSCHEDULED_GROUP.equals(trigger.getGroup())) {
454                LOG.error("Triggers should not be specified for jobs in the unscheduled group - not adding trigger: " + trigger.getName());
455            }
456            else {
457                LOG.info("Adding trigger: " + trigger.getName());
458                try {
459                    scheduler.scheduleJob(trigger);
460                }
461                catch (ObjectAlreadyExistsException ex) {
462                }
463            }
464        }
465        catch (SchedulerException e) {
466            throw new RuntimeException("Caught exception while adding trigger: " + trigger.getFullName(), e);
467        }
468    }
469
470    protected void scheduleJob(String groupName, String jobName, int startStep, int endStep, Date startTime, String requestorEmailAddress, Map<String,String> additionalJobData ) {
471        try {
472            updateStatus(groupName, jobName, SchedulerService.SCHEDULED_JOB_STATUS_CODE);
473            SimpleTriggerDescriptor trigger = new SimpleTriggerDescriptor(jobName, groupName, jobName, dateTimeService);
474            trigger.setStartTime(startTime);
475            Trigger qTrigger = trigger.getTrigger();
476            qTrigger.getJobDataMap().put(JobListener.REQUESTOR_EMAIL_ADDRESS_KEY, requestorEmailAddress);
477            qTrigger.getJobDataMap().put(Job.JOB_RUN_START_STEP, String.valueOf(startStep));
478            qTrigger.getJobDataMap().put(Job.JOB_RUN_END_STEP, String.valueOf(endStep));
479            if ( additionalJobData != null ) {
480                qTrigger.getJobDataMap().putAll(additionalJobData);
481            }
482            for (Trigger oldTrigger : scheduler.getTriggersOfJob(jobName, groupName)) {
483                scheduler.unscheduleJob(oldTrigger.getName(), groupName);
484            }
485            scheduler.scheduleJob(qTrigger);
486        }
487        catch (SchedulerException e) {
488            throw new RuntimeException("Caught exception while scheduling job: " + jobName, e);
489        }
490    }
491
492    protected boolean shouldScheduleJob(JobDetail jobDetail) {
493        try {
494            if (scheduler.getTriggersOfJob(jobDetail.getName(), SCHEDULED_GROUP).length > 0) {
495                return false;
496            }
497            for (String dependencyJobName : getJobDependencies(jobDetail.getName()).keySet()) {
498                JobDetail dependencyJobDetail = getScheduledJobDetail(dependencyJobName);
499                if ( dependencyJobDetail == null ) {
500                    LOG.error( "Unable to get JobDetail for dependency of " + jobDetail.getName() + " : " + dependencyJobName );
501                    return false;
502                }
503                if (!isDependencySatisfiedPositively(jobDetail, dependencyJobDetail)) {
504                    return false;
505                }
506            }
507        }
508        catch (SchedulerException se) {
509            throw new RuntimeException("Caught scheduler exception while determining whether to schedule job: " + jobDetail.getName(), se);
510        }
511        return true;
512    }
513
514    protected boolean shouldCancelJob(JobDetail jobDetail) {
515        LOG.info("shouldCancelJob:::::: " + jobDetail.getFullName());
516        if ( jobDetail == null ) {
517            return true;
518        }
519        for (String dependencyJobName : getJobDependencies(jobDetail.getName()).keySet()) {
520            LOG.info("dependencyJobName:::::" + dependencyJobName);
521            JobDetail dependencyJobDetail = getScheduledJobDetail(dependencyJobName);
522            if (isDependencySatisfiedNegatively(jobDetail, dependencyJobDetail)) {
523                return true;
524            }
525        }
526        return false;
527    }
528
529    protected boolean isDependencySatisfiedPositively(JobDetail dependentJobDetail, JobDetail dependencyJobDetail) {
530        if ( dependentJobDetail == null || dependencyJobDetail == null ) {
531            return false;
532        }
533        return isSucceeded(dependencyJobDetail) || ((isFailed(dependencyJobDetail) || isCancelled(dependencyJobDetail)) && isSoftDependency(dependentJobDetail.getName(), dependencyJobDetail.getName()));
534    }
535
536    protected boolean isDependencySatisfiedNegatively(JobDetail dependentJobDetail, JobDetail dependencyJobDetail) {
537       LOG.info("isDependencySatisfiedNegatively::::  dependentJobDetail::: " + dependencyJobDetail.getFullName() + " dependencyJobDetail    " + dependencyJobDetail.getFullName() );
538        if ( dependentJobDetail == null || dependencyJobDetail == null ) {
539            return true;
540        }
541        return (isFailed(dependencyJobDetail) || isCancelled(dependencyJobDetail)) && !isSoftDependency(dependentJobDetail.getName(), dependencyJobDetail.getName());
542    }
543
544    protected boolean isSoftDependency(String dependentJobName, String dependencyJobName) {
545        return SOFT_DEPENDENCY_CODE.equals(getJobDependencies(dependentJobName).get(dependencyJobName));
546    }
547
548    protected Map<String, String> getJobDependencies(String jobName) {
549        LOG.info("getJobDependencies:::: for job " + jobName);
550        Map<String, String> defaultMap = new HashMap();
551        if (BatchSpringContext.getJobDescriptor(jobName) != null && BatchSpringContext.getJobDescriptor(jobName).getDependencies() != null) {
552            return BatchSpringContext.getJobDescriptor(jobName).getDependencies();
553        } else {
554            return defaultMap;
555        }
556    }
557
558    protected boolean isPending(JobDetail jobDetail) {
559        return getStatus(jobDetail) == null;
560    }
561
562    protected boolean isScheduled(JobDetail jobDetail) {
563        return SCHEDULED_JOB_STATUS_CODE.equals(getStatus(jobDetail));
564    }
565
566    protected boolean isSucceeded(JobDetail jobDetail) {
567        return SUCCEEDED_JOB_STATUS_CODE.equals(getStatus(jobDetail));
568    }
569
570    protected boolean isFailed(JobDetail jobDetail) {
571        return FAILED_JOB_STATUS_CODE.equals(getStatus(jobDetail));
572    }
573
574    protected boolean isCancelled(JobDetail jobDetail) {
575        return CANCELLED_JOB_STATUS_CODE.equals(getStatus(jobDetail));
576    }
577
578    @Override
579    public String getStatus(JobDetail jobDetail) {
580        if ( jobDetail == null ) {
581            return FAILED_JOB_STATUS_CODE;
582        }
583        OleModuleServiceImpl moduleService = (OleModuleServiceImpl)
584            SpringContext.getBean(KualiModuleService.class).getResponsibleModuleServiceForJob(jobDetail.getName());
585        //If the module service has status information for a job, get the status from it
586        //else get status from job detail data map 
587        return (moduleService!=null && moduleService.isExternalJob(jobDetail.getName()))
588                    ? moduleService.getExternalJobStatus(jobDetail.getName())
589                    : jobDetail.getJobDataMap().getString(SchedulerServiceImpl.JOB_STATUS_PARAMETER);
590    }
591
592    protected JobDetail getScheduledJobDetail(String jobName) {
593        LOG.info("getScheduledJobDetail ::::::: " + jobName);
594        try {
595            JobDetail jobDetail = scheduler.getJobDetail(jobName, SCHEDULED_GROUP);
596            if ( jobDetail == null ) {
597                LOG.error( "Unable to obtain the job details for the scheduled version of: " + jobName );
598            }
599            return jobDetail;
600        }
601        catch (SchedulerException e) {
602            throw new RuntimeException("Caught scheduler exception while getting job detail: " + jobName, e);
603        }
604    }
605
606    /**
607     * Sets the scheduler attribute value.
608     * 
609     * @param scheduler The scheduler to set.
610     */
611    @Override
612    public void setScheduler(Scheduler scheduler) {
613        this.scheduler = scheduler;
614    }
615
616    public void setParameterService(ParameterService parameterService) {
617        this.parameterService = parameterService;
618    }
619
620    /**
621     * Sets the dateTimeService attribute value.
622     * 
623     * @param dateTimeService The dateTimeService to set.
624     */
625    public void setDateTimeService(DateTimeService dateTimeService) {
626        this.dateTimeService = dateTimeService;
627    }
628
629    /**
630     * Sets the moduleService attribute value.
631     * 
632     * @param moduleService The moduleService to set.
633     */
634    public void setKualiModuleService(KualiModuleService moduleService) {
635        this.kualiModuleService = moduleService;
636    }
637
638    /**
639     * Sets the jobListener attribute value.
640     * 
641     * @param jobListener The jobListener to set.
642     */
643    public void setJobListener(JobListener jobListener) {
644        this.jobListener = jobListener;
645    }
646
647    @Override
648    public List<BatchJobStatus> getJobs() {
649        ArrayList<BatchJobStatus> jobs = new ArrayList<BatchJobStatus>();
650        try {
651            for (String jobGroup : scheduler.getJobGroupNames()) {
652                for (String jobName : scheduler.getJobNames(jobGroup)) {
653                    try {
654                        JobDescriptor jobDescriptor = retrieveJobDescriptor(jobName);
655                        JobDetail jobDetail = scheduler.getJobDetail(jobName, jobGroup);
656                        jobs.add(new BatchJobStatus(jobDescriptor, jobDetail));
657                    }
658                    catch (NoSuchBeanDefinitionException ex) {
659                        // do nothing, ignore jobs not defined in spring
660                        LOG.warn("Attempt to find bean " + jobGroup + "." + jobName + " failed - not in Spring context");
661                    }
662                }
663            }
664        }
665        catch (SchedulerException ex) {
666            throw new RuntimeException("Exception while obtaining job list", ex);
667        }
668        return jobs;
669    }
670
671    @Override
672    public BatchJobStatus getJob(String groupName, String jobName) {
673        for (BatchJobStatus job : getJobs()) {
674            if (job.getName().equals(jobName) && job.getGroup().equals(groupName)) {
675                return job;
676            }
677        }
678        return null;
679    }
680
681    @Override
682    public List<BatchJobStatus> getJobs(String groupName) {
683        ArrayList<BatchJobStatus> jobs = new ArrayList<BatchJobStatus>();
684        try {
685            for (String jobName : scheduler.getJobNames(groupName)) {
686                try {
687                    JobDescriptor jobDescriptor = retrieveJobDescriptor(jobName);
688                    JobDetail jobDetail = scheduler.getJobDetail(jobName, groupName);
689                    jobs.add(new BatchJobStatus(jobDescriptor, jobDetail));
690                }
691                catch (NoSuchBeanDefinitionException ex) {
692                    // do nothing, ignore jobs not defined in spring
693                    LOG.warn("Attempt to find bean " + groupName + "." + jobName + " failed - not in Spring context");
694                }
695            }
696        }
697        catch (SchedulerException ex) {
698            throw new RuntimeException("Exception while obtaining job list", ex);
699        }
700        return jobs;
701    }
702
703    @Override
704    public List<JobExecutionContext> getRunningJobs() {
705        try {
706            List<JobExecutionContext> jobContexts = scheduler.getCurrentlyExecutingJobs();
707            return jobContexts;
708        }
709        catch (SchedulerException ex) {
710            throw new RuntimeException("Unable to get list of running jobs.", ex);
711        }
712    }
713
714    protected void updateStatus(String groupName, String jobName, String jobStatus) {
715        try {
716            JobDetail jobDetail = scheduler.getJobDetail(jobName, groupName);
717            updateStatus(jobDetail, jobStatus);
718            scheduler.addJob(jobDetail, true);
719        }
720        catch (SchedulerException e) {
721            throw new RuntimeException(new StringBuilder("Caught scheduler exception while updating job status: ").append(jobName).append(", ").append(jobStatus).toString(), e);
722        }
723    }
724
725    @Override
726    public void removeScheduled(String jobName) {
727        try {
728            scheduler.deleteJob(jobName, SCHEDULED_GROUP);
729        }
730        catch (SchedulerException ex) {
731            throw new RuntimeException("Unable to remove scheduled job: " + jobName, ex);
732        }
733    }
734
735    @Override
736    public void addScheduled(JobDetail job) {
737        try {
738            job.setGroup(SCHEDULED_GROUP);
739            scheduler.addJob(job, true);
740        }
741        catch (SchedulerException ex) {
742            throw new RuntimeException("Unable to add job to scheduled group: " + job.getName(), ex);
743        }
744    }
745
746    @Override
747    public void addUnscheduled(JobDetail job) {
748        try {
749            job.setGroup(UNSCHEDULED_GROUP);
750            scheduler.addJob(job, true);
751        }
752        catch (SchedulerException ex) {
753            throw new RuntimeException("Unable to add job to unscheduled group: " + job.getName(), ex);
754        }
755    }
756
757    @Override
758    public List<String> getSchedulerGroups() {
759        try {
760            return Arrays.asList(scheduler.getJobGroupNames());
761        }
762        catch (SchedulerException ex) {
763            throw new RuntimeException("Exception while obtaining job list", ex);
764        }
765    }
766
767    @Override
768    public List<String> getJobStatuses() {
769        return jobStatuses;
770    }
771
772    @Override
773    public void interruptJob(String jobName) {
774        List<JobExecutionContext> runningJobs = getRunningJobs();
775        for (JobExecutionContext jobCtx : runningJobs) {
776            if (jobName.equals(jobCtx.getJobDetail().getName())) {
777                // if so...
778                try {
779                    ((Job) jobCtx.getJobInstance()).interrupt();
780                }
781                catch (UnableToInterruptJobException ex) {
782                    LOG.warn("Unable to perform job interrupt", ex);
783                }
784                break;
785            }
786        }
787
788    }
789
790    @Override
791    public Date getNextStartTime(BatchJobStatus job) {
792        try {
793            Trigger[] triggers = scheduler.getTriggersOfJob(job.getName(), job.getGroup());
794            Date nextDate = new Date(Long.MAX_VALUE);
795            for (Trigger trigger : triggers) {
796                if (trigger.getNextFireTime() != null){
797                if (trigger.getNextFireTime().getTime() < nextDate.getTime()) {
798                    nextDate = trigger.getNextFireTime();
799                }
800            }
801            }
802            if (nextDate.getTime() == Long.MAX_VALUE) {
803                nextDate = null;
804            }
805            return nextDate;
806        }
807        catch (SchedulerException ex) {
808
809        }
810        return null;
811    }
812
813    @Override
814    public Date getNextStartTime(String groupName, String jobName) {
815        BatchJobStatus job = getJob(groupName, jobName);
816
817        return getNextStartTime(job);
818    }
819
820    public void setMailService(MailService mailService) {
821        this.mailService = mailService;
822    }
823    
824    protected JobDescriptor retrieveJobDescriptor(String jobName) {
825        if (externalizedJobDescriptors.containsKey(jobName)) {
826            return externalizedJobDescriptors.get(jobName);
827        }
828        return BatchSpringContext.getJobDescriptor(jobName);
829    }
830    
831    ThreadLocal<List<String>> jobNamesForScheduleJob = new ThreadLocal<List<String>>();
832    
833    protected List<String> getJobNamesForScheduleJob() {
834        List<String> jobNames = new ArrayList<String>();
835        try {
836            for (String scheduledJobName : scheduler.getJobNames(SCHEDULED_GROUP)) {
837                if (scheduler.getTriggersOfJob(scheduledJobName, SCHEDULED_GROUP).length == 0) {
838                    // jobs that have their own triggers will not be included in the master scheduleJob
839                    jobNames.add( scheduledJobName );
840                }
841            }
842        } catch (Exception ex) {
843            LOG.error("Error occurred while initializing job name list", ex);
844        }
845        return jobNames;
846    }
847    
848    @Override
849    public void reinitializeScheduledJobs() {
850        try {
851            for (String scheduledJobName : getJobNamesForScheduleJob() ) {
852                updateStatus(SCHEDULED_GROUP, scheduledJobName, null);
853            }
854        } catch (Exception e) {
855            LOG.error("Error occurred while trying to reinitialize jobs", e);
856        }
857    }
858}