package er.quartzscheduler.util;
import static org.quartz.DateBuilder.futureDate;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.List;
import java.util.Set;
import org.quartz.DateBuilder.IntervalUnit;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.JobListener;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.simpl.SimpleClassLoadHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOObjectStore;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSBundle;
import com.webobjects.foundation.NSMutableArray;
import er.extensions.ERXFrameworkPrincipal;
import er.extensions.foundation.ERXProperties;
import er.quartzscheduler.foundation.ERQSJobDescription;
import er.quartzscheduler.foundation.ERQSJobListener;
import er.quartzscheduler.foundation.ERQSJobSupervisor;
import er.quartzscheduler.foundation.ERQSMyJobListener;
import er.quartzscheduler.foundation.ERQSMySupervisor;
/**
* This framework principal is abstract so you must create you own class, put your code in the abstract method <code>getListOfJobDescription</code>,
* implement the methods newEditingContext(), newEditingContext(osc) and that's it!<p>
* Don't forget to include the following static code in your class:
* <pre>
* static
* {
* log.debug("MyClassThatExtendsCOSchedulerServiceFrameworkPrincipal: static: ENTERED");
* setUpFrameworkPrincipalClass(MyClassThatExtendsCOSchedulerServiceFrameworkPrincipal.class);
* log.debug("MyClassThatExtendsCOSchedulerServiceFrameworkPrincipal: static: DONE");
* }<br>
* </pre>
*
* @author Philippe Rabier
*
*/
public abstract class ERQSSchedulerServiceFrameworkPrincipal extends ERXFrameworkPrincipal
{
public static final String INSTANCE_KEY = "COInstanceKey";
private static final Logger log = LoggerFactory.getLogger(ERQSSchedulerServiceFrameworkPrincipal.class);
private static ERQSSchedulerServiceFrameworkPrincipal sharedInstance;
private volatile Scheduler quartzSheduler;
/**
*
* @return shared instance of framework principal
*/
public static ERQSSchedulerServiceFrameworkPrincipal getSharedInstance()
{
if (sharedInstance == null)
throw new IllegalStateException("method: getSharedInstance: sharedInstance is null.");
return sharedInstance;
}
public static void setSharedInstance(final ERQSSchedulerServiceFrameworkPrincipal aSharedInstance)
{
sharedInstance = aSharedInstance;
}
/**
* Expects that this method never returns null but an empty array if there is no job
*
* @return array of job description to check.
*
*/
public abstract NSArray<? extends ERQSJobDescription> getListOfJobDescription(EOEditingContext editingContext);
/**
* This method is used by a job that subclasses ERQSAbstractJob. It must return an editing context and it's highly recommended that useAutolock() returns false.<br>
* It's also highly recommended to use a new object store coordinator if you work heavily with EOF.
*
* We recommend that you create your own factory as follow:
* <pre>
* private static ERXEC.Factory manualLockingEditingContextFactory = new ERXEC.DefaultFactory() {
protected EOEditingContext _createEditingContext(final EOObjectStore parent)
{
return new MyEditingContext(parent == null ? EOEditingContext.defaultParentObjectStore() : parent)
{
public boolean useAutoLock() {return false;}
public boolean coalesceAutoLocks() {return false;}
};
}
* </pre>
*
* Then implement newEditingContext() as follow:
* <pre>
* public EOEditingContext newEditingContext()
* {
* EOObjectStoreCoordinator osc = ERXTaskObjectStoreCoordinatorPool.objectStoreCoordinator();
* return COEditingContextFactory.newManualLockingEditingContext(osc);
* }
* </pre>
* @return new editingContext
*/
public abstract EOEditingContext newEditingContext();
/**
* This method is used by a job that subclasses ERQSAbstractJob. The first time a job asks for a new editing context, the
* method newEditingContext() is called. Then the following requests call this method by passing the object store coordinator
* used the first time.
*/
public abstract EOEditingContext newEditingContext(EOObjectStore parent);
/**
* This method initializes the scheduler service.<p>
* The following services must be set:
* <ul>
* <li> er.quartzscheduler.schedulerServiceToLaunch=true or false to launch or not the service
* <li> er.quartzscheduler.triggersAutomaticallyPaused=true or false. If <code>true</code> any new job/trigger will be
* in pause mode when added to the scheduler. Very useful when you are developing and debugging your code.
* </ul>
*/
@Override
public void finishInitialization()
{
if (log.isInfoEnabled())
log.info("method: finishInitialization: ENTER: isSchedulerMustRun: {}", schedulerMustRun());
setSharedInstance(this);
if (schedulerMustRun())
{
try
{
Scheduler scheduler = getScheduler();
if (scheduler != null)
{
getScheduler().start();
addJobListener(getDefaultJobListener());
instantiateJobSupervisor();
boolean shouldJobsBePausedAtLaunch = ERXProperties.booleanForKeyWithDefault("er.quartzscheduler.triggersAutomaticallyPaused", false);
if (shouldJobsBePausedAtLaunch)
getScheduler().pauseAll();
}
log.info("method: finishInitialization: DONE. {}", scheduler == null ? "The scheduler is not running." : "The scheduler has been successfully launched.");
} catch (SchedulerException e)
{
log.error("method: finishInitialization: error message: {}", e.getMessage(), e);
}
}
else
log.info("method: finishInitialization: DONE. The scheduler is not running.");
}
/**
* This method reads the the property er.quartzscheduler.schedulerServiceToLaunch<p>
* It's a static method because the sharedInstance could be null if it's not running.
*
* @return <code>true</code> if the scheduler should run, <code>false</code> by default.
*/
public static boolean schedulerMustRun()
{
return ERXProperties.booleanForKeyWithDefault("er.quartzscheduler.schedulerServiceToLaunch", false);
}
/**
* Return the quartz scheduler which schedules the job described by ERQSJobDescription objects.<p>
* As the quartz scheduler is uses a ram job stores, everything is gone when the scheduler stops.<br>
* The persistence must be handled by your own EOs which must implement ERQSJobDescription interface.<p>
* You have several options to set quartz properties:
* <ul>
* <li>do nothing. Quartz will use the default property file quartz.properties in quartz.jar
* <li>define the property "org.quartz.properties" with the full path of your property file
* <li>define the properties "quartz.properties.fileName" and "quartz.properties.framework". If "quartz.properties.framework" is missing
* the mainBundle is used to find the filePath of your property file.
* </ul>
*
* @return the scheduler
* @see ERQSJobDescription
*/
public Scheduler getScheduler()
{
if (quartzSheduler == null)
{
try
{
String propFileName = ERXProperties.stringForKey("quartz.properties.fileName");
// Grab the Scheduler instance from the Factory
// There is no synchronized mechanism because the ivar quartzSheduler is initialized when finishInitialization is called.
if (propFileName != null)
{
String propFramework = ERXProperties.stringForKey("quartz.properties.framework");
NSBundle bundle;
String filePath = null;
URL propFileURL = null;
if (propFramework == null)
bundle = NSBundle.mainBundle();
else
bundle = NSBundle.bundleForName(propFramework);
if (bundle != null)
propFileURL = bundle.pathURLForResourcePath(propFileName);
if (propFileURL == null)
log.error("method: getScheduler: unable to get the path to the properties file: {} in the framework: {}.\nThe Quartz scheduler is not launched.", propFileName, propFramework);
else
{
filePath = propFileURL.getFile();
StdSchedulerFactory sf = new StdSchedulerFactory();
sf.initialize(filePath);
quartzSheduler = sf.getScheduler();
}
}
else
quartzSheduler = StdSchedulerFactory.getDefaultScheduler();
} catch (SchedulerException e)
{
log.error("method: getScheduler: exception", e);
}
}
return quartzSheduler;
}
/**
* Return a list of all jobs handled by the scheduler, even those that are not managed by the framework, aka
* jobs that have been added manually.
*
* @return immutable list of all job detail or an empty list
*/
public List<JobDetail> getAllJobs()
{
try
{
NSMutableArray<JobDetail> jobDetailList = new NSMutableArray<>();
List<String> groups = getScheduler().getJobGroupNames();
for (int i = 0; i < groups.size(); i++)
{
String name = groups.get(i);
GroupMatcher<JobKey> matcher = GroupMatcher.groupEquals(name);
Set<JobKey> keys = getScheduler().getJobKeys(matcher);
for (JobKey jk : keys)
{
JobDetail jd = getScheduler().getJobDetail(jk);
jobDetailList.add(jd);
}
}
return jobDetailList.immutableClone();
} catch (SchedulerException e)
{
log.error("method: getAllJobs: execution error.", e);
}
return NSArray.emptyArray();
}
public boolean hasRunningJobs()
{
List<JobExecutionContext> executingJobs = null;
try
{
executingJobs = getScheduler().getCurrentlyExecutingJobs();
} catch (SchedulerException e)
{
log.error("method: hasRunningJobs: execution error", e);
}
return executingJobs != null && executingJobs.size() > 0;
}
public Trigger.TriggerState getTriggerState(final JobKey aJobKey)
{
Trigger aTrigger = getTriggerOfJob(aJobKey);
if (aTrigger == null)
return Trigger.TriggerState.NONE;
try
{
return getScheduler().getTriggerState(aTrigger.getKey());
} catch (SchedulerException e)
{
log.error("method: getTriggerState: error for JobKey: {}", aJobKey, e);
}
return Trigger.TriggerState.NONE;
}
public Trigger getTriggerOfJob(final JobKey aJobKey)
{
try
{
if (getScheduler().getTriggersOfJob(aJobKey).size() > 0)
return getScheduler().getTriggersOfJob(aJobKey).get(0);
} catch (SchedulerException e)
{
log.error("method: getTriggerOfJob: error for JobKey: {}", aJobKey, e);
}
return null;
}
public void triggerNow(final JobDetail aJob) throws SchedulerException
{
getScheduler().triggerJob(aJob.getKey(), aJob.getJobDataMap());
}
protected void instantiateJobSupervisor()
{
Class<? extends Job> supervisorClass = ERQSJobSupervisor.class;
if (this.getClass().isAnnotationPresent(ERQSMySupervisor.class))
{
String supervisorClassPath = this.getClass().getAnnotation(ERQSMySupervisor.class).value();
SimpleClassLoadHelper loader = new SimpleClassLoadHelper();
try
{
supervisorClass = (Class<? extends Job>) loader.loadClass(supervisorClassPath);
} catch (ClassNotFoundException e)
{
log.error("method: instantiateJobSupervisor: load class error for supervisorClass: {}", supervisorClassPath, e);
}
}
JobDataMap map = new JobDataMap();
map.put(INSTANCE_KEY, getSharedInstance());
JobDetail job = newJob(supervisorClass).withIdentity("JobSupervisor", Scheduler.DEFAULT_GROUP).usingJobData(map).build();
Trigger trigger = newTrigger()
.withIdentity("JobSupervisorTrigger")
.startAt(futureDate(1, IntervalUnit.MINUTE))
.withPriority(Trigger.DEFAULT_PRIORITY)
.withSchedule(simpleSchedule()
.withIntervalInMinutes(supervisorSleepDuration())
.repeatForever())
.build();
// Attache data to the job
try
{
getScheduler().scheduleJob(job, trigger);
} catch (SchedulerException e)
{
log.error("method: instantiateJobSupervisor: unable to launch supervisor.", e);
}
}
/**
* Use this method if you need to add other listeners jobs handled by COScheduler, aka job with group
* beginning with ERQSJobSupervisor.GROUP_NAME_PREFIX
*
* @param newJobListener
*/
protected void addJobListener(final JobListener newJobListener)
{
try
{
GroupMatcher<JobKey> matcher = GroupMatcher.groupStartsWith(ERQSJobSupervisor.GROUP_NAME_PREFIX);
getScheduler().getListenerManager().addJobListener(newJobListener, matcher);
} catch (SchedulerException e)
{
log.error("method: addJobListener: unable to add a job listener", e);
}
}
/**
* This method returns a joblistener object. If you used annotation to change the default job listener, getDefaultJobListener()
* will return an instance of your own class. Otherwise, it will return a ERQSJobListener object
*
* @return a JobListener object
*/
protected JobListener getDefaultJobListener()
{
JobListener aJobListener = null;
Class<? extends JobListener> jobListenerClass = ERQSJobListener.class;
if (this.getClass().isAnnotationPresent(ERQSMyJobListener.class))
{
String jobListenerClassPath = this.getClass().getAnnotation(ERQSMyJobListener.class).value();
SimpleClassLoadHelper loader = new SimpleClassLoadHelper();
loader.initialize();
try
{
jobListenerClass = (Class<? extends JobListener>) loader.loadClass(jobListenerClassPath);
} catch (ClassNotFoundException e)
{
log.error("method: getDefaultJobListener: load class error for jobListenerClass: {}", jobListenerClassPath, e);
}
}
if (jobListenerClass != null)
{
Constructor<? extends JobListener> constructor = null;
try
{
constructor = jobListenerClass.getConstructor(ERQSSchedulerServiceFrameworkPrincipal.class);
aJobListener = constructor.newInstance(ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance());
} catch (SecurityException se)
{
log.error("method: createJobInstance: getConstructor error", se);
} catch (NoSuchMethodException nme)
{
log.error("method: createJobInstance: getConstructor error: ", nme);
} catch (IllegalArgumentException e)
{
log.error("method: createJobInstance: newInstance error", e);
} catch (InstantiationException e)
{
log.error("method: createJobInstance: getConstructor error: ", e);
} catch (IllegalAccessException e)
{
log.error("method: createJobInstance: newInstance error", e);
} catch (InvocationTargetException e)
{
log.error("method: createJobInstance: newInstance error", e);
}
}
return aJobListener;
}
protected int supervisorSleepDuration()
{
return ERXProperties.intForKeyWithDefault("er.quartzscheduler.COJobSupervisor.sleepduration", ERQSJobSupervisor.DEFAULT_SLEEP_DURATION);
}
public synchronized void deleteAllJobs()
{
List<JobDetail> allJobs = getAllJobs();
if (allJobs.size() > 0)
{
NSMutableArray<JobKey> jobKeys = new NSMutableArray<>(allJobs.size());
for (JobDetail jobDetail : allJobs)
{
jobKeys.add(jobDetail.getKey());
}
try
{
getScheduler().deleteJobs(jobKeys);
} catch (SchedulerException e)
{
log.error("method: deleteAllJobs", e);
}
}
}
public synchronized void stopScheduler()
{
try
{
getScheduler().shutdown();
} catch (SchedulerException e)
{
log.error("method: stopScheduler: exception: {}", e.getMessage(), e);
}
}
}