org.sakaiproject.component.app.scheduler.SchedulerManagerImpl Maven / Gradle / Ivy
/**********************************************************************************
* $URL$
* $Id$
**********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.
*
**********************************************************************************/
package org.sakaiproject.component.app.scheduler;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.quartz.CronScheduleBuilder;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.JobListener;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerListener;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
import org.sakaiproject.api.app.scheduler.ConfigurableJobProperty;
import org.sakaiproject.api.app.scheduler.ConfigurableJobPropertyValidationException;
import org.sakaiproject.api.app.scheduler.ConfigurableJobPropertyValidator;
import org.sakaiproject.api.app.scheduler.JobBeanWrapper;
import org.sakaiproject.api.app.scheduler.SchedulerManager;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.component.app.scheduler.jobs.SpringConfigurableJobBeanWrapper;
import org.sakaiproject.component.app.scheduler.jobs.SpringInitialJobSchedule;
import org.sakaiproject.component.app.scheduler.jobs.SpringJobBeanWrapper;
import org.sakaiproject.db.api.SqlService;
public class SchedulerManagerImpl implements SchedulerManager
{
private static final Logger LOG = LoggerFactory.getLogger(SchedulerManagerImpl.class);
public final static String
SCHEDULER_LOADJOBS = "scheduler.loadjobs";
private DataSource dataSource;
private String serverId;
private Set qrtzJobs;
private Map qrtzQualifiedJobs = new TreeMap(); // map for SelectItems
/** The properties file from the classpath */
private String qrtzPropFile;
/** The properties file from sakai.home */
private String qrtzPropFileSakai;
private Properties qrtzProperties;
private TriggerListener globalTriggerListener;
private Boolean autoDdl;
private boolean startScheduler = true;
private Map beanJobs = new Hashtable();
private static final String JOB_INTERFACE = "org.quartz.Job";
private static final String STATEFULJOB_INTERFACE = "org.quartz.StatefulJob";
// Service dependencies
private ServerConfigurationService serverConfigurationService;
private SchedulerFactory schedFactory;
private Scheduler scheduler;
private SqlService sqlService;
private LinkedList
globalTriggerListeners = new LinkedList();
private LinkedList
globalJobListeners = new LinkedList();
private LinkedList
initialJobSchedule = null;
public void init()
{
try
{
qrtzProperties = initQuartzConfiguration();
qrtzProperties.setProperty("org.quartz.scheduler.instanceId", serverId);
// note: becuase job classes are jarred , it is impossible to iterate
// through a directory by calling listFiles on a file object.
// Therefore, we need the class list list from spring.
// find quartz jobs from specified 'qrtzJobs' and verify they
// that these jobs implement the Job interface
Iterator qrtzJobsIterator = qrtzJobs.iterator();
while (qrtzJobsIterator.hasNext())
{
String className = (String) qrtzJobsIterator.next();
Class cl = null;
try
{
cl = Class.forName(className);
}
catch (ClassNotFoundException e)
{
LOG.warn("Could not locate class: " + className + " on classpath");
}
if (cl != null)
{
// check that each class implements the Job interface
if (doesImplementJobInterface(cl))
{
qrtzQualifiedJobs.put(cl.getName(), cl.getName());
}
else
{
LOG.warn("Class: " + className
+ " does not implement quartz Job interface");
}
}
}
// run ddl
if (autoDdl.booleanValue()){
try
{
sqlService.ddl(this.getClass().getClassLoader(), "quartz2");
}
catch (Throwable t)
{
LOG.warn(this + ".init(): ", t);
}
}
boolean isInitialStartup = isInitialStartup(sqlService);
if (isInitialStartup && autoDdl.booleanValue())
{
LOG.info("Performing initial population of the Quartz tables.");
sqlService.ddl(this.getClass().getClassLoader(), "init_locks2");
}
/*
Determine whether or not to load the jobs defined in the initialJobSchedules list. These jobs will be loaded
under the following conditions:
1) the server configuration property "scheduler.loadjobs" is "true"
2) "scheduler.loadjobs" is "init" and this is the first startup for the scheduler (eg. this is a new Sakai instance)
"scheduler.loadjobs" is set to "init" by default
*/
String
loadJobs = serverConfigurationService.getString(SCHEDULER_LOADJOBS, "init").trim();
List
initSchedules = getInitialJobSchedules();
boolean
loadInitSchedules = (initSchedules != null) && (initSchedules.size() > 0) &&
(("init".equalsIgnoreCase(loadJobs) && isInitialStartup) ||
"true".equalsIgnoreCase(loadJobs));
if (loadInitSchedules)
LOG.debug ("Preconfigured jobs will be loaded");
else
LOG.debug ("Preconfigured jobs will not be loaded");
// start scheduler and load jobs
schedFactory = new StdSchedulerFactory(qrtzProperties);
scheduler = schedFactory.getScheduler();
// loop through persisted jobs removing both the job and associated
// triggers for jobs where the associated job class is not found
Set jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(Scheduler.DEFAULT_GROUP));
for (JobKey key : jobKeys) {
try
{
JobDetail detail = scheduler.getJobDetail(key);
String bean = detail.getJobDataMap().getString(JobBeanWrapper.SPRING_BEAN_NAME);
Job job = (Job) ComponentManager.get(bean);
if (job == null) {
LOG.warn("scheduler cannot load class for persistent job:" + key);
scheduler.deleteJob(key);
LOG.warn("deleted persistent job:" + key);
}
}
catch (SchedulerException e)
{
LOG.warn("scheduler cannot load class for persistent job:" + key);
scheduler.deleteJob(key);
LOG.warn("deleted persistent job:" + key);
}
}
for (TriggerListener tListener : globalTriggerListeners)
{
scheduler.getListenerManager().addTriggerListener(tListener);
}
for (JobListener jListener : globalJobListeners)
{
scheduler.getListenerManager().addJobListener(jListener);
}
if (loadInitSchedules)
{
LOG.debug ("Loading preconfigured jobs");
loadInitialSchedules();
}
//scheduler.addGlobalTriggerListener(globalTriggerListener);
if (isStartScheduler()) {
scheduler.start();
} else {
LOG.info("Scheduler Not Started, startScheduler=false");
}
}
catch (Exception e)
{
LOG.error("Failed to start scheduler.", e);
throw new IllegalStateException("Scheduler cannot start!", e);
}
}
/**
* This loads the configurations for quartz.
* It loads the defaults from the classpath and then loads override values from
* sakai.home.
* @return The quartz properties.
* @throws IOException When we can't load the default values.
*/
private Properties initQuartzConfiguration() throws IOException
{
InputStream propertiesInputStream = null;
Properties properties = new Properties();
// load the default quartz properties file
// if this fails we want to propogate the error to stop startup.
try
{
propertiesInputStream = this.getClass().getResourceAsStream(qrtzPropFile);
properties.load(propertiesInputStream);
}
finally
{
if (propertiesInputStream != null)
{
try
{
propertiesInputStream.close();
}
catch (IOException e)
{
LOG.debug("Failed to close stream.", e);
}
}
}
// load the configuration out of sakai home
// any failures here shouldn't result in startup failing.
File file = new File(serverConfigurationService.getSakaiHomePath(), qrtzPropFileSakai);
if (file.exists() && file.isFile()) {
try
{
propertiesInputStream = new FileInputStream(file);
properties.load(propertiesInputStream);
LOG.info("Loaded extra configuration from: "+ file.getAbsolutePath());
}
catch (IOException e)
{
LOG.warn("Failed to load file: "+ file, e);
}
finally
{
if (propertiesInputStream != null)
{
try
{
propertiesInputStream.close();
}
catch (IOException e)
{
LOG.debug("Failed to close stream.", e);
}
}
}
}
return properties;
}
private boolean doesImplementJobInterface(Class cl)
{
Class[] classArr = cl.getInterfaces();
for (int i = 0; i < classArr.length; i++)
{
if (classArr[i].getName().equals(JOB_INTERFACE) ||
classArr[i].getName().equals(STATEFULJOB_INTERFACE))
{
return true;
}
}
return false;
}
/**
* Runs an SQL select statement to determine if the Quartz lock rows exist in the database. If the rows do not exist
* this method assumes this is the first time the scheduler has been started. The select statement will be defined
* in the {vendor}/checkTables.sql file within the shared library deployed by this project. The statement should be
* of the form "SELECT COUNT(*) from QUARTZ_LOCKS;". If the count is zero it is assumed this is a new install.
* If the count is non-zero it is assumed the QUARTZ_LOCKS table has been initialized and this is not a new install.
*
* @param sqlService
* @return
*/
private boolean isInitialStartup(SqlService sqlService)
{
String
checkTablesScript = sqlService.getVendor() + "/checkTables.sql";
ClassLoader
loader = this.getClass().getClassLoader();
String
chkStmt = null;
InputStream
in = null;
BufferedReader
r = null;
try
{
// find the resource from the loader
in = loader.getResourceAsStream(checkTablesScript);
r = new BufferedReader(new InputStreamReader(in));
chkStmt = r.readLine();
}
catch (Exception e)
{
LOG.error("Could not read the file " + checkTablesScript + " to determine if this is a new installation. Preconfigured jobs will only be loaded if the server property scheduler.loadjobs is \"true\"", e);
return false;
}
finally
{
try
{
r.close();
}
catch (Exception e){}
try
{
in.close();
}
catch (Exception e){}
}
List l = sqlService.dbRead(chkStmt);
if (l != null && l.size() > 0)
{
return (l.get(0).equalsIgnoreCase("0"));
}
else
{
return false;
}
}
/**
* Loads jobs and schedules triggers for preconfigured jobs.
*/
private void loadInitialSchedules()
{
for (SpringInitialJobSchedule sched : getInitialJobSchedules())
{
SpringJobBeanWrapper
wrapper = sched.getJobBeanWrapper();
LOG.debug ("Loading schedule for preconfigured job \"" + wrapper.getJobType() + "\"");
JobDetail jd = JobBuilder.newJob(wrapper.getJobClass())
.withIdentity(sched.getJobName(), Scheduler.DEFAULT_GROUP)
.storeDurably()
.requestRecovery()
.build();
JobDataMap map = jd.getJobDataMap();
map.put(JobBeanWrapper.SPRING_BEAN_NAME, wrapper.getBeanId());
map.put(JobBeanWrapper.JOB_TYPE, wrapper.getJobType());
if (SpringConfigurableJobBeanWrapper.class.isAssignableFrom(wrapper.getClass()))
{
SpringConfigurableJobBeanWrapper
confJob = (SpringConfigurableJobBeanWrapper) wrapper;
ConfigurableJobPropertyValidator
validator = confJob.getConfigurableJobPropertyValidator();
Map
conf = sched.getConfiguration();
boolean
fail = false;
for (ConfigurableJobProperty cProp : confJob.getConfigurableJobProperties())
{
String
key = cProp.getLabelResourceKey(),
val = conf.get(key);
LOG.debug ("job property '" + key + "' is set to '" + val + "'");
if (val == null && cProp.isRequired())
{
val = cProp.getDefaultValue();
if (val == null)
{
LOG.error ("job property '" + key + "' is required but has no value; job '" + sched.getJobName() + "' of type '" + wrapper.getJobClass() + "' will not be configured");
fail = true;
break;
}
LOG.debug ("job property '" + key + "' set to default value '" + val + "'");
}
if (val != null)
{
try
{
validator.assertValid(key, val);
}
catch (ConfigurableJobPropertyValidationException cjpve)
{
LOG.error ("job property '" + key + "' was set to an invalid value '" + val + "'; job '" + sched.getJobName() + "' of type '" + wrapper.getJobClass() + "' will not be configured");
fail = true;
break;
}
map.put (key, val);
}
}
if (fail) continue;
}
try
{
scheduler.addJob(jd, false);
}
catch (SchedulerException e)
{
LOG.error ("Failed to schedule job '" + sched.getJobName() + "' of type '" + wrapper.getJobClass() + "'");
continue;
}
Trigger trigger = null;
trigger = TriggerBuilder.newTrigger()
.withIdentity(sched.getTriggerName(), Scheduler.DEFAULT_GROUP)
.forJob(jd.getKey())
.withSchedule(CronScheduleBuilder.cronSchedule(sched.getCronExpression()))
.build();
try
{
scheduler.scheduleJob(trigger);
}
catch (SchedulerException e)
{
LOG.error ("Trigger could not be scheduled. Failed to schedule job '" + sched.getJobName() + "' of type '" + wrapper.getJobClass() + "'");
}
}
}
/**
* @see org.sakaiproject.api.app.scheduler.SchedulerManager#destroy()
*/
public void destroy()
{
try{
if (!scheduler.isShutdown()){
scheduler.shutdown();
}
}
catch (Throwable t){
LOG.error("An error occurred while stopping the scheduler", t);
}
}
public List getInitialJobSchedules()
{
return initialJobSchedule;
}
public void setInitialJobSchedules(List jobSchedule)
{
if(jobSchedule == null || jobSchedule.size() < 1)
return;
this.initialJobSchedule = new LinkedList ();
initialJobSchedule.addAll(jobSchedule);
}
/**
* @deprecated use {@link #setGlobalTriggerListeners(Set)}
* @return Returns the globalTriggerListener.
*/
public TriggerListener getGlobalTriggerListener()
{
return globalTriggerListener;
}
/**
* @deprecated use {@link #getGlobalTriggerListeners()}
* @param globalTriggerListener The globalTriggerListener to set.
*/
public void setGlobalTriggerListener(TriggerListener globalTriggerListener)
{
this.globalTriggerListener = globalTriggerListener;
if (globalTriggerListeners != null)
{
globalTriggerListeners.addFirst(globalTriggerListener);
}
}
public void setGlobalTriggerListeners (final List listeners)
{
globalTriggerListeners.clear();
if (globalTriggerListener != null)
{
globalTriggerListeners.add(globalTriggerListener);
}
if (listeners != null)
{
globalTriggerListeners.addAll(listeners);
}
}
public List getGlobalTriggerListeners()
{
return Collections.unmodifiableList(globalTriggerListeners);
}
public void setGlobalJobListeners (final List listeners)
{
globalJobListeners.clear();
if (listeners != null)
{
globalJobListeners.addAll(listeners);
}
}
public List getGlobalJobListeners()
{
return Collections.unmodifiableList(globalJobListeners);
}
/**
* @return Returns the serverId.
*/
public String getServerId()
{
return serverId;
}
/**
* @param serverId The serverId to set.
*/
public void setServerId(String serverId)
{
this.serverId = serverId;
}
/**
* @return Returns the dataSource.
*/
public DataSource getDataSource()
{
return dataSource;
}
/**
* @param dataSource The dataSource to set.
*/
public void setDataSource(DataSource dataSource)
{
this.dataSource = dataSource;
}
public void setSqlService(SqlService sqlService)
{
this.sqlService = sqlService;
}
/**
* @return Returns the qrtzQualifiedJobs.
*/
public Map getQrtzQualifiedJobs()
{
return qrtzQualifiedJobs;
}
/**
* @param qrtzQualifiedJobs The qrtzQualifiedJobs to set.
*/
public void setQrtzQualifiedJobs(Map qrtzQualifiedJobs)
{
this.qrtzQualifiedJobs = qrtzQualifiedJobs;
}
/**
* @return Returns the qrtzJobs.
*/
public Set getQrtzJobs()
{
return qrtzJobs;
}
/**
* @param qrtzJobs The qrtzJobs to set.
*/
public void setQrtzJobs(Set qrtzJobs)
{
this.qrtzJobs = qrtzJobs;
}
/**
* @return Returns the qrtzPropFile.
*/
public String getQrtzPropFile()
{
return qrtzPropFile;
}
/**
* @param qrtzPropFile The qrtzPropFile to set.
*/
public void setQrtzPropFile(String qrtzPropFile)
{
this.qrtzPropFile = qrtzPropFile;
}
public void setQrtzPropFileSakai(String qrtzPropFileSakai)
{
this.qrtzPropFileSakai = qrtzPropFileSakai;
}
/**
* @return Returns the scheduler.
*/
public Scheduler getScheduler()
{
return scheduler;
}
/**
* @param scheduler The sched to set.
*/
public void setScheduler(Scheduler scheduler)
{
this.scheduler = scheduler;
}
/**
* @param serverConfigurationService The ServerConfigurationService to get our configuation from.
*/
public void setServerConfigurationService(ServerConfigurationService serverConfigurationService)
{
this.serverConfigurationService = serverConfigurationService;
}
/**
* @see org.sakaiproject.api.app.scheduler.SchedulerManager#setAutoDdl(java.lang.Boolean)
*/
public void setAutoDdl(Boolean b)
{
autoDdl = b;
}
public Map getBeanJobs() {
return beanJobs;
}
public void registerBeanJob(String jobName, JobBeanWrapper job) {
getBeanJobs().put(jobName, job);
}
public JobBeanWrapper getJobBeanWrapper(String beanWrapperId) {
return (JobBeanWrapper) getBeanJobs().get(beanWrapperId);
}
public boolean isStartScheduler() {
return startScheduler;
}
public void setStartScheduler(boolean startScheduler) {
this.startScheduler = startScheduler;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy