/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.sys.batch; import java.net.InetAddress; import java.net.UnknownHostException; import java.text.ParseException; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Appender; import org.apache.log4j.Logger; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.batch.service.SchedulerService; import org.kuali.kfs.sys.context.ProxyUtils; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.kfs.sys.service.impl.KfsParameterConstants; import org.kuali.rice.core.api.CoreConstants; import org.kuali.rice.core.api.datetime.DateTimeService; import org.kuali.rice.coreservice.framework.parameter.ParameterService; import org.kuali.rice.kew.api.exception.WorkflowException; import org.kuali.rice.krad.UserSession; import org.kuali.rice.krad.util.GlobalVariables; import org.kuali.rice.krad.util.KRADConstants; import org.quartz.InterruptableJob; import org.quartz.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.StatefulJob; import org.quartz.UnableToInterruptJobException; import org.springframework.util.StopWatch; public class Job implements StatefulJob, InterruptableJob { public static final String JOB_RUN_START_STEP = "JOB_RUN_START_STEP"; public static final String JOB_RUN_END_STEP = "JOB_RUN_END_STEP"; public static final String MASTER_JOB_NAME = "MASTER_JOB_NAME"; public static final String STEP_RUN_PARM_NM = "RUN_IND"; public static final String STEP_RUN_ON_DATE_PARM_NM = "RUN_DATE"; public static final String STEP_USER_PARM_NM = "USER"; public static final String RUN_DATE_CUTOFF_PARM_NM = "RUN_DATE_CUTOFF_TIME"; private static final Logger LOG = Logger.getLogger(Job.class); private SchedulerService schedulerService; private ParameterService parameterService; private DateTimeService dateTimeService; private List<Step> steps; private Step currentStep; private Appender ndcAppender; private boolean notRunnable; private transient Thread workerThread; /** * @see org.quartz.Job#execute(org.quartz.JobExecutionContext) */ @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { workerThread = Thread.currentThread(); if (isNotRunnable()) { if (LOG.isInfoEnabled()) { LOG.info("Skipping job because doNotRun is true: " + jobExecutionContext.getJobDetail().getName()); } return; } int startStep = 0; try { startStep = Integer.parseInt(jobExecutionContext.getMergedJobDataMap().getString(JOB_RUN_START_STEP)); } catch (NumberFormatException ex) { // not present, do nothing } int endStep = 0; try { endStep = Integer.parseInt(jobExecutionContext.getMergedJobDataMap().getString(JOB_RUN_END_STEP)); } catch (NumberFormatException ex) { // not present, do nothing } Date jobRunDate = dateTimeService.getCurrentDate(); int currentStepNumber = 0; try { LOG.info("Executing job: " + jobExecutionContext.getJobDetail() + " on machine " + getMachineName() + " scheduler instance id " + jobExecutionContext.getScheduler().getSchedulerInstanceId() + "\n" + jobDataMapToString(jobExecutionContext.getJobDetail().getJobDataMap())); for (Step step : getSteps()) { currentStepNumber++; // prevent starting of the next step if the thread has an interrupted status if (workerThread.isInterrupted()) { LOG.warn("Aborting Job execution due to manual interruption"); schedulerService.updateStatus(jobExecutionContext.getJobDetail(), SchedulerService.CANCELLED_JOB_STATUS_CODE); return; } if (startStep > 0 && currentStepNumber < startStep) { if (LOG.isInfoEnabled()) { LOG.info("Skipping step " + currentStepNumber + " - startStep=" + startStep); } continue; // skip to next step } else if (endStep > 0 && currentStepNumber > endStep) { if (LOG.isInfoEnabled()) { LOG.info("Ending step loop - currentStepNumber=" + currentStepNumber + " - endStep = " + endStep); } break; } step.setInterrupted(false); try { if (!runStep(parameterService, jobExecutionContext.getJobDetail().getFullName(), currentStepNumber, step, jobRunDate)) { break; } } catch (InterruptedException ex) { LOG.warn("Stopping after step interruption"); schedulerService.updateStatus(jobExecutionContext.getJobDetail(), SchedulerService.CANCELLED_JOB_STATUS_CODE); return; } if (step.isInterrupted()) { LOG.warn("attempt to interrupt step failed, step continued to completion"); LOG.warn("cancelling remainder of job due to step interruption"); schedulerService.updateStatus(jobExecutionContext.getJobDetail(), SchedulerService.CANCELLED_JOB_STATUS_CODE); return; } } } catch (Exception e) { schedulerService.updateStatus(jobExecutionContext.getJobDetail(), SchedulerService.FAILED_JOB_STATUS_CODE); throw new JobExecutionException("Caught exception in " + jobExecutionContext.getJobDetail().getName(), e, false); } LOG.info("Finished executing job: " + jobExecutionContext.getJobDetail().getName()); schedulerService.updateStatus(jobExecutionContext.getJobDetail(), SchedulerService.SUCCEEDED_JOB_STATUS_CODE); } public static boolean runStep(ParameterService parameterService, String jobName, int currentStepNumber, Step step, Date jobRunDate) throws InterruptedException, WorkflowException { boolean continueJob = true; if (GlobalVariables.getUserSession() == null) { LOG.info(new StringBuffer("Started processing step: ").append(currentStepNumber).append("=").append(step.getName()).append(" for user <unknown>")); } else { LOG.info(new StringBuffer("Started processing step: ").append(currentStepNumber).append("=").append(step.getName()).append(" for user ").append(GlobalVariables.getUserSession().getPrincipalName())); } if (!skipStep(parameterService, step, jobRunDate)) { Step unProxiedStep = (Step) ProxyUtils.getTargetIfProxied(step); Class<?> stepClass = unProxiedStep.getClass(); GlobalVariables.clear(); String stepUserName = KFSConstants.SYSTEM_USER; if (parameterService.parameterExists(stepClass, STEP_USER_PARM_NM)) { stepUserName = parameterService.getParameterValueAsString(stepClass, STEP_USER_PARM_NM); } if (LOG.isInfoEnabled()) { LOG.info(new StringBuffer("Creating user session for step: ").append(step.getName()).append("=").append(stepUserName)); } GlobalVariables.setUserSession(new UserSession(stepUserName)); if (LOG.isInfoEnabled()) { LOG.info(new StringBuffer("Executing step: ").append(step.getName()).append("=").append(stepClass)); } StopWatch stopWatch = new StopWatch(); stopWatch.start(jobName); try { continueJob = step.execute(jobName, jobRunDate); } catch (InterruptedException e) { LOG.error("Exception occured executing step", e); throw e; } catch (RuntimeException e) { LOG.error("Exception occured executing step", e); throw e; } stopWatch.stop(); LOG.info(new StringBuffer("Step ").append(step.getName()).append(" of ").append(jobName).append(" took ").append(stopWatch.getTotalTimeSeconds() / 60.0).append(" minutes to complete").toString()); if (!continueJob) { LOG.info("Stopping job after successful step execution"); } } LOG.info(new StringBuffer("Finished processing step ").append(currentStepNumber).append(": ").append(step.getName())); return continueJob; } /** * This method determines whether the Job should not run the Step based on the RUN_IND and RUN_DATE Parameters. * When RUN_IND exists and equals 'Y' it takes priority and does not consult RUN_DATE. * If RUN_DATE exists, but contains an empty value the step will not be skipped. */ protected static boolean skipStep(ParameterService parameterService, Step step, Date jobRunDate) { Step unProxiedStep = (Step) ProxyUtils.getTargetIfProxied(step); Class<?> stepClass = unProxiedStep.getClass(); //RUN_IND takes priority: when RUN_IND exists and RUN_IND=Y always run the Step //RUN_DATE: when RUN_DATE exists, but the value is empty run the Step final boolean runIndExists = parameterService.parameterExists(stepClass, STEP_RUN_PARM_NM); if (runIndExists) { final boolean runInd = parameterService.getParameterValueAsBoolean(stepClass, STEP_RUN_PARM_NM); if (!runInd) { if (LOG.isInfoEnabled()) { LOG.info("Skipping step due to system parameter: " + STEP_RUN_PARM_NM +" for "+ stepClass.getName()); } return true; // RUN_IND is false - let's skip } } final boolean runDateExists = parameterService.parameterExists(stepClass, STEP_RUN_ON_DATE_PARM_NM); if (runDateExists) { final boolean runDateIsEmpty = StringUtils.isEmpty(parameterService.getParameterValueAsString(stepClass, STEP_RUN_ON_DATE_PARM_NM)); if (runDateIsEmpty) { return false; // run date param is empty, so run the step } final DateTimeService dTService = SpringContext.getBean(DateTimeService.class); final Collection<String> runDates = parameterService.getParameterValuesAsString(stepClass, STEP_RUN_ON_DATE_PARM_NM); boolean matchedRunDate = false; final String[] cutOffTime = parameterService.parameterExists(KfsParameterConstants.FINANCIAL_SYSTEM_BATCH.class, RUN_DATE_CUTOFF_PARM_NM) ? StringUtils.split(parameterService.getParameterValueAsString(KfsParameterConstants.FINANCIAL_SYSTEM_BATCH.class, RUN_DATE_CUTOFF_PARM_NM), ':') : new String[] { "00", "00", "00"}; // no cutoff time param? Then default to midnight of tomorrow for (String runDate: runDates) { try { if (withinCutoffWindowForDate(jobRunDate, dTService.convertToDate(runDate), dTService, cutOffTime)) { matchedRunDate = true; } } catch (ParseException pe) { LOG.error("ParseException occured parsing " + runDate, pe); } } // did we fail to match a run date? then skip this step if (!matchedRunDate) { if (LOG.isInfoEnabled()) { LOG.info("Skipping step due to system parameters: " + STEP_RUN_PARM_NM + ", " + STEP_RUN_ON_DATE_PARM_NM + " and " + RUN_DATE_CUTOFF_PARM_NM + " for "+ stepClass.getName()); } return true; } } //run step return false; } /** * Checks if the current jobRunDate is within the cutoff window for the given run date from the RUN_DATE parameter. * The window is defined as midnight of the date specified in the parameter to the RUN_DATE_CUTOFF_TIME of the next day. * * @param jobRunDate the time the job is attempting to start * @param runDateToCheck the current member of the appropriate RUN_DATE to check * @param dateTimeService an instance of the DateTimeService * @return true if jobRunDate is within the current runDateToCheck window, false otherwise */ protected static boolean withinCutoffWindowForDate(Date jobRunDate, Date runDateToCheck, DateTimeService dateTimeService, String[] cutOffWindow) { final Calendar jobRunCalendar = dateTimeService.getCalendar(jobRunDate); final Calendar beginWindow = getCutoffWindowBeginning(runDateToCheck, dateTimeService); final Calendar endWindow = getCutoffWindowEnding(runDateToCheck, dateTimeService, cutOffWindow); return jobRunCalendar.after(beginWindow) && jobRunCalendar.before(endWindow); } /** * Defines the beginning of the cut off window * * @param runDateToCheck the run date which defines the cut off window * @param dateTimeService an implementation of the DateTimeService * @return the begin date Calendar of the cutoff window */ protected static Calendar getCutoffWindowBeginning(Date runDateToCheck, DateTimeService dateTimeService) { Calendar beginWindow = dateTimeService.getCalendar(runDateToCheck); beginWindow.set(Calendar.HOUR_OF_DAY, 0); beginWindow.set(Calendar.MINUTE, 0); beginWindow.set(Calendar.SECOND, 0); beginWindow.set(Calendar.MILLISECOND, 0); return beginWindow; } /** * Defines the end of the cut off window * * @param runDateToCheck the run date which defines the cut off window * @param dateTimeService an implementation of the DateTimeService * @param cutOffTime an Array in the form of [hour, minute, second] when the cutoff window ends * @return the end date Calendar of the cutoff window */ protected static Calendar getCutoffWindowEnding(Date runDateToCheck, DateTimeService dateTimeService, String[] cutOffTime) { Calendar endWindow = dateTimeService.getCalendar(runDateToCheck); endWindow.add(Calendar.DAY_OF_YEAR, 1); endWindow.set(Calendar.HOUR_OF_DAY, Integer.parseInt(cutOffTime[0])); endWindow.set(Calendar.MINUTE, Integer.parseInt(cutOffTime[1])); endWindow.set(Calendar.SECOND, Integer.parseInt(cutOffTime[2])); return endWindow; } /* This code is likely no longer reference, but was not removed, due to the fact that institutions may be calling */ /** * @deprecated "Implementing institutions likely want to call Job#withinCutoffWindowForDate" */ public static boolean isPastCutoffWindow(Date date, Collection<String> runDates) { DateTimeService dTService = SpringContext.getBean(DateTimeService.class); ParameterService parameterService = SpringContext.getBean(ParameterService.class); Calendar jobRunDate = dTService.getCalendar(date); if (parameterService.parameterExists(KfsParameterConstants.FINANCIAL_SYSTEM_BATCH.class, RUN_DATE_CUTOFF_PARM_NM)) { String[] cutOffTime = StringUtils.split(parameterService.getParameterValueAsString(KfsParameterConstants.FINANCIAL_SYSTEM_BATCH.class, RUN_DATE_CUTOFF_PARM_NM), ':'); Calendar runDate = null; for (String runDateStr : runDates) { try { runDate = dTService.getCalendar(dTService.convertToDate(runDateStr)); runDate.add(Calendar.DAY_OF_YEAR, 1); runDate.set(Calendar.HOUR_OF_DAY, Integer.parseInt(cutOffTime[0])); runDate.set(Calendar.MINUTE, Integer.parseInt(cutOffTime[1])); runDate.set(Calendar.SECOND, Integer.parseInt(cutOffTime[2])); } catch (ParseException e) { LOG.error("ParseException occured parsing " + runDateStr, e); } if (jobRunDate.before(runDate)) { return false; } } } return true; } /** * @throws UnableToInterruptJobException */ @Override public void interrupt() throws UnableToInterruptJobException { // ask the step to interrupt if (currentStep != null) { currentStep.interrupt(); } // also attempt to interrupt the thread, to cause an InterruptedException if the step ever waits or sleeps workerThread.interrupt(); } public void setParameterService(ParameterService parameterService) { this.parameterService = parameterService; } public void setSteps(List<Step> steps) { this.steps = steps; } public Appender getNdcAppender() { return ndcAppender; } public void setNdcAppender(Appender ndcAppender) { this.ndcAppender = ndcAppender; } public void setNotRunnable(boolean notRunnable) { this.notRunnable = notRunnable; } protected boolean isNotRunnable() { return notRunnable; } public ParameterService getParameterService() { return parameterService; } public List<Step> getSteps() { return steps; } public void setSchedulerService(SchedulerService schedulerService) { this.schedulerService = schedulerService; } public void setDateTimeService(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } protected String jobDataMapToString(JobDataMap jobDataMap) { StringBuilder buf = new StringBuilder(); buf.append("{"); Iterator keys = jobDataMap.keySet().iterator(); boolean hasNext = keys.hasNext(); while (hasNext) { String key = (String) keys.next(); Object value = jobDataMap.get(key); buf.append(key).append("="); if (value == jobDataMap) { buf.append("(this map)"); } else { buf.append(value); } hasNext = keys.hasNext(); if (hasNext) { buf.append(", "); } } buf.append("}"); return buf.toString(); } protected String getMachineName() { try { return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { return "Unknown"; } } }