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