package er.quartzscheduler.foundation;
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
import java.util.List;
import java.util.Set;
import org.quartz.CronTrigger;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.ScheduleBuilder;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOKeyGlobalID;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSMutableSet;
import com.webobjects.foundation.NSSet;
import er.extensions.eof.ERXEC;
import er.extensions.eof.ERXGenericRecord;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXStringUtilities;
import er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal;
/**
* The supervisor has in charge to add, remove or update the list of job handled by the quartz scheduler.<p>
* Every job handled by the supervisor has a group starting by GROUP_NAME_PREFIX. The goal is to let developers to add any
* job directly, aka not linked to a job description. For that reason, by convention, the jobs not handled by the
* supervisor must have a group not starting with GROUP_NAME_PREFIX
*
* @author Philippe Rabier
*
*/
@DisallowConcurrentExecution
public class ERQSJobSupervisor extends ERQSAbstractJob
{
public static final String TRIGGER_SUFFIX = ERXProperties.stringForKeyWithDefault("er.quartzscheduler.foundation.ERQSJobSupervisor.suffix", ".CO");
public static final int DEFAULT_SLEEP_DURATION = 10; //10 mn
public static final String GROUP_NAME_PREFIX = ERXProperties.stringForKeyWithDefault("er.quartzscheduler.foundation.ERQSJobSupervisor.prefix", "CO.");
@Override
public void execute(final JobExecutionContext jobexecutioncontext) throws JobExecutionException
{
super.execute(jobexecutioncontext);
EOEditingContext ec = editingContext();
ec.lock();
try
{
NSArray<? extends ERQSJobDescription> jobs2Check = getSchedulerFPInstance().getListOfJobDescription(ec);
setResultMessage("# of jobs to check: " + jobs2Check.size());
if (log.isDebugEnabled())
log.debug("method: execute: jobs2Check.size: " + jobs2Check.size());
removeObsoleteJobs(jobs2Check);
if (jobs2Check.size() != 0)
addOrModifyJobs(jobs2Check);
} catch (Exception e)
{
log.error("method: execute: fetching jobs.", e);
}
finally
{
ec.unlock();
ec.dispose();
}
}
/**
* Return a a set of jobs handled currently by Quartz. Actually, it's a set of JobKey rather than Job.
*
* @return set of JobKeys, never return null but an empty set instead.
*/
protected Set<JobKey> getScheduledJobKeys()
{
Set<JobKey> scheduledJobKeys = null;
try
{
GroupMatcher<JobKey> matcher = GroupMatcher.groupStartsWith(GROUP_NAME_PREFIX);
scheduledJobKeys = getScheduler().getJobKeys(matcher);
} catch (SchedulerException e)
{
log.error("method: getScheduledJobKeys: unable to get the list.", e);
}
return scheduledJobKeys == null ? new java.util.HashSet<JobKey>(0) : scheduledJobKeys;
}
/**
* From jobs2Check (a fresh list of ERQSJobDescription objects), removeJobs checks if jobs must be removed.<p>
*
* @param jobs2Check list of ERQSJobDescription objects
*/
protected void removeObsoleteJobs(final NSArray<? extends ERQSJobDescription> jobs2Check)
{
NSSet<JobKey> jobKeys2remove;
NSSet<JobKey> scheduledJobKeysSet = new NSSet<JobKey>(getScheduledJobKeys());
// If the list of existing jobs is empty, nothing to remove
if (scheduledJobKeysSet.size() != 0)
{
// If there is no new job, we must remove all existing jobs
if (jobs2Check.size() == 0)
jobKeys2remove = scheduledJobKeysSet;
else
{
//NSSet<JobKey> scheduledJobKeysSet = new NSSet<JobKey>(scheduledJobKeys);
//JobKey temp = scheduledJobKeysSet.anyObject();
NSMutableSet<JobKey> jobKeys2Check = new NSMutableSet<JobKey>(jobs2Check.count());
for (ERQSJobDescription aJob2Check : jobs2Check)
{
JobKey aJobKey = getJobKeyForJobDescription(aJob2Check);
jobKeys2Check.add(aJobKey);
}
jobKeys2remove = scheduledJobKeysSet.setBySubtractingSet(jobKeys2Check);
}
if (log.isDebugEnabled())
log.debug("method: removeJobs: jobKeys2remove.size: " + jobKeys2remove.size());
if (jobKeys2remove.size() != 0)
{
setResultMessage("# of jobs to remove: " + jobKeys2remove.size());
try
{
getScheduler().deleteJobs(jobKeys2remove.allObjects());
} catch (SchedulerException e)
{
log.error("method: removeJobs: unable to remove the jobs.", e);
}
}
}
}
/**
* From jobs2Check (a fresh list of ERQSJobDescription objects), addOrModifyJobs checks if jobs must be added or modified.<p>
*
* @param jobs2Check list of ERQSJobDescription objects
*/
protected void addOrModifyJobs(final NSArray<? extends ERQSJobDescription> jobs2Check)
{
setResultMessage("# of jobs to add or modify: " + jobs2Check.size());
for (ERQSJobDescription aJob2Check : jobs2Check)
{
JobKey aJobKey = getJobKeyForJobDescription(aJob2Check);
try
{
JobDetail aJobDetail = getScheduler().getJobDetail(aJobKey);
if (log.isDebugEnabled())
log.debug("method: jobs2AddOrModify: aJobKey: " + aJobKey + " /aJobDetail in scheduler: " + aJobDetail);
if (aJobDetail == null)
addJob2Scheduler(aJob2Check);
else
modifyJob(aJob2Check, aJobDetail);
} catch (SchedulerException e)
{
log.error("method: addOrModifyJobs: error when retrieving a jobDetail with this jobKey: " + aJobKey, e);
}
}
}
public JobKey getJobKeyForJobDescription(final ERQSJobDescription aJobDescription)
{
return new JobKey(aJobDescription.name(), buildGroup(aJobDescription.group()));
}
/**
* Add a job to the scheduler described the job description job2Add.<p>
*
* @param job2Add job to add
*/
protected void addJob2Scheduler(final ERQSJobDescription job2Add)
{
if (!isJobDescriptionValid(job2Add))
throw new IllegalArgumentException("method: addJob2Scheduler: some fields of job2Add are null or empty: job2Check: " + job2Add);
else
{
JobDetail job = buildJobDetail(job2Add);
if (log.isDebugEnabled())
log.debug("method: addJob2Scheduler: job: " + job);
if (job != null)
{
Trigger trigger;
try
{
trigger = buildTriggerForJob(job2Add, job);
getScheduler().scheduleJob(job, trigger);
}
catch (SchedulerException se)
{
log.error("method: addJob2Scheduler: unable to schedule the job: " + job2Add.group() + "." + job2Add.name(), se);
}
}
}
}
protected void modifyJob(final ERQSJobDescription job2Check, final JobDetail job)
{
if (log.isDebugEnabled())
log.debug("method: modifyJob: ENTER: job2Check: " + job2Check + " /job: " +job);
if (!isJobDescriptionValid(job2Check))
throw new IllegalArgumentException("method: applyModification2Scheduler: some fields of job2Check are null or empty: job2Check: " + job2Check);
// We compare the job description with the scheduled job
// We don't compare to the name and group because the job would have been removed and added just before.
Scheduler scheduler = getScheduler();
String jobClass = job.getJobClass().getName();
String jobDescription = job.getDescription();
String jobCronExpression;
boolean isJobModified = (!ERXStringUtilities.stringEqualsString(job2Check.jobDescription(), jobDescription) || !job2Check.classPath().equals(jobClass));
try
{
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(job.getKey());
if (triggers.size() != 0 && triggers.get(0) instanceof CronTrigger)
{
CronTrigger aTrigger = (CronTrigger) triggers.get(0);
jobCronExpression = aTrigger.getCronExpression();
if (!ERXStringUtilities.stringEqualsString(job2Check.cronExpression(), jobCronExpression) && !isJobModified)
{
//We just need to reschedule the job
Trigger newTrigger = buildTriggerForJob(job2Check, job);
TriggerKey aTriggerKey = new TriggerKey(buildTriggerName(job2Check.name()), buildGroup(job2Check.group()));
scheduler.rescheduleJob(aTriggerKey, newTrigger);
if (log.isDebugEnabled())
log.debug("method: modifyJob: job2Check: " + job2Check + " has been rescheduled.");
}
if (isJobModified)
{
if (log.isDebugEnabled())
log.debug("method: modifyJob: job2Check: " + job2Check + " has been removed then added.");
// We remove the job and we create a new one
getScheduler().deleteJob(job.getKey());
addJob2Scheduler(job2Check);
}
}
} catch (SchedulerException e)
{
log.error("method: modifyJob: unable to get triggers of job: " + job2Check.group() + "." + job2Check.name(), e);
}
if (log.isDebugEnabled())
log.debug("method: modifyJob: DONE: job2Check: " + job2Check + " /job: " +job + " /isJobModified: " + isJobModified);
}
/**
* Return a job detail built from a ERQSJobDescription object
*
* @param jobDescription
* @return a JobDetail object
*/
protected JobDetail buildJobDetail(final ERQSJobDescription jobDescription)
{
JobDataMap map = new JobDataMap();
map.put(ERQSSchedulerServiceFrameworkPrincipal.INSTANCE_KEY, getSchedulerFPInstance());
if (jobDescription.isEnterpriseObject())
{
EOKeyGlobalID globalID = ((ERXGenericRecord)jobDescription).permanentGlobalID();
map.put(ERQSJob.ENTERPRISE_OBJECT_KEY, globalID);
}
else
map.put(ERQSJob.NOT_PERSISTENT_OBJECT_KEY, jobDescription);
String name = jobDescription.name();
String group = jobDescription.group();
String classPath = jobDescription.classPath();
String description = jobDescription.jobDescription();
JobDetail job = null;
Class<? extends Job> jobClass = getClass(classPath);
if (jobClass != null)
{
job = newJob(jobClass)
.withIdentity(name, buildGroup(group))
.withDescription(description)
.usingJobData(map)
.build();
}
if (jobDescription.jobInfos() != null)
job.getJobDataMap().putAll(jobDescription.jobInfos());
return job;
}
/**
* Return a trigger built from a ERQSJobDescription object and a JobDetail object
*
* @param jobDescription (we suppose that jobDescription is a subclass of ERXGenericRecord or a non persistent object)
* @param job
* @return a Trigger object
*/
protected Trigger buildTriggerForJob(final ERQSJobDescription jobDescription, final JobDetail job)
{
String name = jobDescription.name();
String group = jobDescription.group();
String cronExpression = jobDescription.cronExpression();
return buildTrigger(name, group, cronExpression, null, job);
}
protected Trigger buildTrigger(final String name, final String group, final String cronExpression, final JobDataMap map, final JobDetail job)
{
Trigger trigger = null;
ScheduleBuilder<? extends Trigger> scheduleBuilder = null;
if (cronExpression != null)
{
try
{
scheduleBuilder = cronSchedule(cronExpression);
} catch (RuntimeException e)
{
log.error("method: buildTrigger: cronExpression: " + cronExpression + " for name: " + name + " /group: " + group, e);
}
}
else
scheduleBuilder = simpleSchedule();
trigger = newTrigger()
.withIdentity(buildTriggerName(name), buildGroup(group))
.withPriority(Trigger.DEFAULT_PRIORITY)
.forJob(job)
.usingJobData(map == null ? new JobDataMap() : map)
.withSchedule(scheduleBuilder)
.build();
return trigger;
}
protected String buildTriggerName(final String name)
{
return name + TRIGGER_SUFFIX;
}
protected String buildGroup(final String group)
{
if (ERXStringUtilities.stringIsNullOrEmpty(group))
return GROUP_NAME_PREFIX + Scheduler.DEFAULT_GROUP;
return GROUP_NAME_PREFIX + group;
}
protected boolean isJobDescriptionValid(final ERQSJobDescription aJobDescription)
{
return (aJobDescription.classPath() != null && aJobDescription.classPath().length() != 0
&& aJobDescription.name() != null && aJobDescription.name().length() != 0
);
}
protected Class<? extends Job> getClass(final String path) {
Class<? extends Job> jobClass = null;
try
{
jobClass = (Class<? extends Job>) Class.forName(path, false, this.getClass().getClassLoader());
}
catch (ClassNotFoundException ce)
{
log.error("method: getClass: path: " + path + " /exception: " + ce.getMessage(), ce);
}
catch (ExceptionInInitializerError ie)
{
log.error("method: getClass: path: " + path + " /exception: " + ie.getMessage(), ie);
}
catch (LinkageError le)
{
log.error("method: getClass: path: " + path + " /exception: " + le.getMessage(), le);
}
return jobClass;
}
@Override
public EOEditingContext newEditingContext()
{
return ERXEC.newEditingContext();
}
}