/* * Copyright (c) 2005-2011 Grameen Foundation USA * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. * * See also http://www.apache.org/licenses/LICENSE-2.0.html for an * explanation of the license and how it is applied. */ package org.mifos.framework.components.batchjobs; import java.text.ParseException; import java.util.Date; import java.util.LinkedList; import java.util.List; import org.mifos.framework.components.batchjobs.exceptions.BatchJobException; import org.mifos.framework.util.DateTimeService; import org.quartz.CronTrigger; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.Scheduler; import org.quartz.SimpleTrigger; import org.quartz.StatefulJob; import org.quartz.Trigger; import org.quartz.TriggerUtils; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.configuration.JobLocator; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRepository; import org.springframework.scheduling.quartz.QuartzJobBean; public abstract class MifosBatchJob extends QuartzJobBean implements StatefulJob { /** A key to store and retrieve the launch date from JobParameters map. */ public static final String JOB_EXECUTION_TIME_KEY = "executionTime"; private static boolean batchJobRunning = false; private static boolean requiresExclusiveAccess = true; private JobLauncher jobLauncher; private JobLocator jobLocator; private JobExplorer jobExplorer; private JobRepository jobRepository; public void setJobLauncher(JobLauncher jobLauncher) { this.jobLauncher = jobLauncher; } public void setJobLocator(JobLocator jobLocator) { this.jobLocator = jobLocator; } public void setJobExplorer(JobExplorer jobExplorer) { this.jobExplorer = jobExplorer; } public void setJobRepository(JobRepository jobRepository) { this.jobRepository = jobRepository; } @Override public void executeInternal(JobExecutionContext context) throws JobExecutionException { try { String jobName = context.getJobDetail().getName(); Job job = jobLocator.getJob(jobName); catchUpMissedLaunches(job, context); checkAndLaunchJob(job, getJobParametersFromContext(context), 0); } catch(Exception ex) { throw new JobExecutionException(ex); } } /** * A method responsible for the actual launch of the Spring Batch job. * @param job Job class * @param jobParameters Job parameters * @return Batch computation status * @throws BatchJobException when something goes wrong */ private BatchStatus launchJob(Job job, JobParameters jobParameters) throws BatchJobException { BatchStatus exitStatus = BatchStatus.UNKNOWN; JobExecution jobExecution = null; try { batchJobStarted(); requiresExclusiveAccess(); jobExecution = jobLauncher.run(job, jobParameters); exitStatus = jobExecution.getStatus(); } catch(JobInstanceAlreadyCompleteException jiace) { exitStatus = BatchStatus.COMPLETED; return exitStatus; } catch(Exception ex) { throw new BatchJobException(ex); } finally { batchJobFinished(); } return exitStatus; } /** * This method is a wrapper around launchJob method. It checks whether previous * runs of the job executed successfully and attempts to re-run them in case they did not. * @param job Job class * @param jobParameters Job parameters * @param lookUpDepth Counter used to track current recurrence depth * @return Batch computation status * @throws BatchJobException when something goes wrong */ public BatchStatus checkAndLaunchJob(Job job, JobParameters jobParameters, int lookUpDepth) throws BatchJobException { List<JobInstance> jobInstances = jobExplorer.getJobInstances(job.getName(), lookUpDepth, lookUpDepth+1); if(jobInstances.size() == 0) { return launchJob(job, jobParameters); } JobInstance jobInstance = jobInstances.get(0); List<JobExecution> jobExecutions = jobExplorer.getJobExecutions(jobInstance); JobExecution jobExecution = jobExecutions.get(0); // latest execution if(jobExecution.getStatus() == BatchStatus.COMPLETED) { return launchJob(job, jobParameters); } checkAndLaunchJob(job, jobExecution.getJobInstance().getJobParameters(), lookUpDepth+1); return launchJob(job, jobParameters); } public void catchUpMissedLaunches(Job job, JobExecutionContext context) throws Exception { List<JobInstance> jobInstances = jobExplorer.getJobInstances(job.getName(), 0, 1); if(jobInstances.size() > 0) { JobInstance jobInstance = jobInstances.get(0); Date previousFireTime = new Date(jobInstance.getJobParameters().getLong(JOB_EXECUTION_TIME_KEY)); Date scheduledFireTime = context.getScheduledFireTime(); Trigger trigger = context.getTrigger(); boolean onDemandRun = false; if (Scheduler.DEFAULT_MANUAL_TRIGGERS.equals(trigger.getGroup())) { // this is a manual run trigger = context.getScheduler().getTrigger(job.getName(), Scheduler.DEFAULT_GROUP); scheduledFireTime = new DateTimeService().getCurrentDateTime().toDate(); onDemandRun = true; } List<Date> missedLaunches = computeMissedJobLaunches(previousFireTime, scheduledFireTime, trigger, onDemandRun); for(Date missedLaunch : missedLaunches) { JobParameters jobParameters = createJobParameters(missedLaunch.getTime()); launchJob(job, jobParameters); } } } @SuppressWarnings("unchecked") public List<Date> computeMissedJobLaunches(Date from, Date to, Trigger trigger, boolean onDemandRun) throws Exception { List<Date> missedLaunches = new LinkedList<Date>(); if(trigger instanceof CronTrigger) { CronTrigger cronTrigger = new CronTrigger(); cronTrigger.setStartTime(from); cronTrigger.setNextFireTime(from); String cronExpression = ((CronTrigger)trigger).getCronExpression(); try { cronTrigger.setCronExpression(cronExpression); } catch(ParseException pe) { throw new Exception(pe); } List<Date> computationOutcome = TriggerUtils.computeFireTimesBetween(cronTrigger, null, from, to); missedLaunches.addAll(computationOutcome); missedLaunches.remove(0); if (!onDemandRun && missedLaunches.size() > 0) { missedLaunches.remove(missedLaunches.size()-1); } } else if (trigger instanceof SimpleTrigger) { SimpleTrigger simpleTrigger = new SimpleTrigger(); simpleTrigger.setStartTime(from); simpleTrigger.setNextFireTime(from); simpleTrigger.setRepeatInterval(((SimpleTrigger)trigger).getRepeatInterval()); simpleTrigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); List<Date> computationOutcome = TriggerUtils.computeFireTimesBetween(simpleTrigger, null, from, to); missedLaunches.addAll(computationOutcome); missedLaunches.remove(0); if (!onDemandRun && missedLaunches.size() > 0) { missedLaunches.remove(missedLaunches.size()-1); } } return missedLaunches; } public static JobParameters getJobParametersFromContext(JobExecutionContext context) { JobParametersBuilder builder = new JobParametersBuilder(); if (Scheduler.DEFAULT_MANUAL_TRIGGERS.equals(context.getTrigger().getGroup())) { // this is a manual run builder.addLong(JOB_EXECUTION_TIME_KEY, new DateTimeService().getCurrentDateTime().getMillis()); } else { builder.addLong(JOB_EXECUTION_TIME_KEY, context.getScheduledFireTime().getTime()); } return builder.toJobParameters(); } public static JobParameters createJobParameters(long scheduledLaunchTime) { JobParametersBuilder builder = new JobParametersBuilder(); builder.addLong(JOB_EXECUTION_TIME_KEY, scheduledLaunchTime); return builder.toJobParameters(); } /** * Classes inheriting from MifosBatchJob must override this method and * return an appropriate Helper class containing business logic. * * @return Helper class containing business logic */ public abstract TaskHelper getTaskHelper(); /** * This method determines if users can continue to use the system while this task/batch job is running. * <br /> * Override this method and return false it exclusive access is not necessary. */ public void requiresExclusiveAccess() { MifosBatchJob.batchJobRequiresExclusiveAccess(true); } public static boolean isBatchJobRunning() { return batchJobRunning; } public static boolean isBatchJobRunningThatRequiresExclusiveAccess() { return batchJobRunning && requiresExclusiveAccess; } public static void batchJobStarted() { batchJobRunning = true; requiresExclusiveAccess = true; } public static void batchJobFinished() { batchJobRunning = false; } public static Boolean isExclusiveAccessRequired() { return requiresExclusiveAccess; } public static void batchJobRequiresExclusiveAccess(Boolean setting) { requiresExclusiveAccess = setting; } }