/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-2006 The eXist team
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* $Id$
*/
package org.exist.scheduler;
import org.apache.log4j.Logger;
import org.exist.EXistException;
import org.exist.security.SecurityManager;
import org.exist.security.User;
import org.exist.storage.BrokerPool;
import org.exist.storage.SystemTask;
import org.exist.util.Configuration;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Vector;
/**
* A Scheduler to trigger Startup, System and User defined jobs
*
* @author Adam Retter <adam.retter@devon.gov.uk>
*/
public class Scheduler
{
public static final String CONFIGURATION_ELEMENT_NAME = "scheduler";
public static final String CONFIGURATION_JOB_ELEMENT_NAME = "job";
public static final String JOB_TYPE_ATTRIBUTE = "type";
public static final String JOB_CLASS_ATTRIBUTE = "class";
public static final String JOB_XQUERY_ATTRIBUTE = "xquery";
public static final String JOB_CRON_TRIGGER_ATTRIBUTE = "cron-trigger";
public static final String JOB_PERIOD_ATTRIBUTE = "period";
public static final String JOB_DELAY_ATTRIBUTE = "delay";
public static final String JOB_REPEAT_ATTRIBUTE = "repeat";
public static final String CONFIGURATION_JOB_PARAMETER_ELEMENT_NAME = "parameter";
public static final String PROPERTY_SCHEDULER_JOBS = "scheduler.jobs";
public static final String JOB_TYPE_USER = "user";
public static final String JOB_TYPE_STARTUP = "startup";
public static final String JOB_TYPE_SYSTEM = "system";
public static final String JOB_NAME_ATTRIBUTE = "name";
//the scheduler
private org.quartz.Scheduler scheduler = null;
//startup jobs
private Vector startupJobs = new Vector();
private BrokerPool brokerpool = null;
private Configuration config = null;
private final static Logger LOG = Logger.getLogger(Scheduler.class); //Logger
/**
* Create and Start a new Scheduler
*
* @param brokerpool The brokerpool for which this scheduler is intended
*/
public Scheduler(BrokerPool brokerpool, Configuration config) throws EXistException
{
this.brokerpool = brokerpool;
this.config = config;
try
{
//load the properties for quartz
InputStream is = Scheduler.class.getResourceAsStream("quartz.properties");
Properties properties = new Properties();
try
{
properties.load(is);
}
catch (IOException e)
{
throw new EXistException("Failed to load scheduler settings from org/exist/scheduler/quartz.properties");
}
properties.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, brokerpool.getId() + "_QuartzScheduler");
SchedulerFactory schedulerFactory = new StdSchedulerFactory(properties);
scheduler = schedulerFactory.getScheduler();
}
catch(SchedulerException se)
{
throw new EXistException("Unable to create Scheduler", se);
}
}
public void run()
{
try
{
setupConfiguredJobs();
executeStartupJobs();
scheduler.start();
}
catch(SchedulerException se)
{
LOG.error(se);
}
}
/**
* Shutdown the running Scheduler
*
* Asynchronous method. use isShutdown() to determine if the
* Scheduler has Shutdown
*/
public void shutdown(boolean waitForJobsToComplete)
{
try
{
scheduler.shutdown(waitForJobsToComplete);
}
catch(SchedulerException se)
{
LOG.warn(se);
}
}
public boolean isShutdown()
{
try
{
return scheduler.isShutdown();
}
catch(SchedulerException se)
{
LOG.warn(se);
return false;
}
}
/**
* Creates a startup job
*
* @param job The job to trigger at startup
* @param params Any parameters to pass to the job
*/
private void createStartupJob(UserJob job, Properties params)
{
//Create the job details
JobDetail jobDetail = new JobDetail(job.getName(), job.getGroup(), job.getClass());
//Setup the job's data map
JobDataMap jobDataMap = jobDetail.getJobDataMap();
setupJobDataMap(job, jobDataMap, params);
//create the minimum quartz supporting classes to execute a job
SimpleTrigger trig = new SimpleTrigger();
trig.setJobDataMap(jobDataMap);
JobExecutionContext jec = new JobExecutionContext(null, new org.quartz.spi.TriggerFiredBundle(jobDetail, trig, null, false, null, null, null, null), job);
startupJobs.add(jec);
}
/**
* Executes all startup jobs
*/
public void executeStartupJobs()
{
for(Iterator itStartupJob = startupJobs.iterator(); itStartupJob.hasNext();)
{
JobExecutionContext jec = (JobExecutionContext)itStartupJob.next();
org.quartz.Job j = jec.getJobInstance();
if(LOG.isInfoEnabled())
LOG.info("Running startup job '" + jec.getJobDetail().getName() + "'");
try
{
//execute the job
j.execute(jec);
}
catch(SchedulerException se)
{
LOG.error("Unable to run startup job '" + jec.getJobDetail().getName() + "'", se);
}
}
}
/**
* @param period The period, in milliseconds.
* @param job The job to trigger after each period
* @param delay <= 0, start now, otherwise start in specified number of milliseconds
*
*
* @return true if the job was successfully scheduled, false otherwise
*/
public boolean createPeriodicJob(long period, JobDescription job, long delay)
{
return createPeriodicJob(period, job, delay, null, SimpleTrigger.REPEAT_INDEFINITELY);
}
/**
* @param period The period, in milliseconds.
* @param job The job to trigger after each period
* @param delay <= 0, start now, otherwise start in specified number of milliseconds
* @param params Any parameters to pass to the job
*
* @return true if the job was successfully scheduled, false otherwise
*/
public boolean createPeriodicJob(long period, JobDescription job, long delay, Properties params)
{
return createPeriodicJob(period, job, delay, params, SimpleTrigger.REPEAT_INDEFINITELY);
}
/**
* @param period The period, in milliseconds.
* @param job The job to trigger after each period
* @param delay <= 0, start now, otherwise start in specified number of milliseconds
* @param params Any parameters to pass to the job
* @param repeatCount Number of times to repeat this job.
*
* @return true if the job was successfully scheduled, false otherwise
*/
public boolean createPeriodicJob(long period, JobDescription job, long delay, Properties params, int repeatCount)
{
//Create the job details
JobDetail jobDetail = new JobDetail(job.getName(), job.getGroup(), job.getClass());
//Setup the job's data map
JobDataMap jobDataMap = jobDetail.getJobDataMap();
setupJobDataMap(job, jobDataMap, params);
//setup a trigger for the job, millisecond based
SimpleTrigger trigger = new SimpleTrigger();
trigger.setRepeatInterval(period);
trigger.setRepeatCount(repeatCount);
//when should the trigger start
if(delay <= 0)
{
//start now
trigger.setStartTime(new Date());
}
else
{
//start after period
Calendar start = Calendar.getInstance();
start.add(Calendar.MILLISECOND, (int)delay);
trigger.setStartTime(start.getTime());
}
//set the trigger's name
trigger.setName(job.getName() + " Trigger");
//schedule the job
try
{
scheduler.scheduleJob(jobDetail, trigger);
}
catch(SchedulerException se)
{
//Failed to schedule Job
LOG.error("Failed to schedule periodic job '" + job.getName() + "'", se);
return false;
}
//Successfully scheduled Job
return true;
}
/**
* @param cronExpression The Cron scheduling expression
* @param job The job to trigger after each period
*
* @return true if the job was successfully scheduled, false otherwise
*/
public boolean createCronJob(String cronExpression, JobDescription job)
{
return createCronJob(cronExpression, job, null);
}
/**
* @param cronExpression The Cron scheduling expression
* @param job The job to trigger after each period
* @param params Any parameters to pass to the job
*
* @return true if the job was successfully scheduled, false otherwise
*/
public boolean createCronJob(String cronExpression, JobDescription job, Properties params)
{
//Create the job details
JobDetail jobDetail = new JobDetail(job.getName(), job.getGroup(), job.getClass());
//Setup the job's data map
JobDataMap jobDataMap = jobDetail.getJobDataMap();
setupJobDataMap(job, jobDataMap, params);
try
{
//setup a trigger for the job, Cron based
CronTrigger trigger = new CronTrigger(job.getName() + " Trigger", job.getGroup(), cronExpression);
//schedule the job
scheduler.scheduleJob(jobDetail, trigger);
}
catch(ParseException pe)
{
//Failed to schedule Job
LOG.error("Failed to schedule cron job '" + job.getName() + "'", pe);
return false;
}
catch(SchedulerException se)
{
//Failed to schedule Job
LOG.error("Failed to schedule cron job '" + job.getName() + "'", se);
return false;
}
//Successfully scheduled Job
return true;
}
/**
* Removes a Job from the Scheduler
*
* @param jobName The name of the Job
* @param jobGroup The group that the Job was Scheduled in
*
* @return true if the job was deleted, false otherwise
*/
public boolean deleteJob(String jobName, String jobGroup)
{
try
{
return scheduler.deleteJob(jobName, jobGroup);
}
catch(SchedulerException se)
{
LOG.error("Failed to delete job '" + jobName + "'", se);
return false;
}
}
/**
* Pauses a Job with the Scheduler
*
* @param jobName The name of the Job
* @param jobGroup The group that the Job was Scheduled in
*/
public boolean pauseJob(String jobName, String jobGroup)
{
try
{
scheduler.pauseJob(jobName, jobGroup);
return true;
}
catch(SchedulerException se)
{
LOG.error("Failed to pause job '" + jobName + "'", se);
return false;
}
}
/**
* Resume a Job with the Scheduler
*
* @param jobName The name of the Job
* @param jobGroup The group that the Job was Scheduled in
*/
public boolean resumeJob(String jobName, String jobGroup)
{
try
{
scheduler.resumeJob(jobName, jobGroup);
return true;
}
catch(SchedulerException se)
{
LOG.error("Failed to resume job '" + jobName + "'", se);
return false;
}
}
/**
* Gets the names of the Job groups
*
* @return String array of the Job group names
*/
public String[] getJobGroupNames()
{
try
{
return scheduler.getJobGroupNames();
}
catch(SchedulerException se)
{
LOG.error("Failed to get job group names", se);
return null;
}
}
/**
* Gets information about currently Scheduled Jobs
*
* @return An array of ScheduledJobInfo
*/
public ScheduledJobInfo[] getScheduledJobs()
{
ArrayList jobs = new ArrayList();
try
{
//get the trigger groups
String[] trigGroups = scheduler.getTriggerGroupNames();
for(int tg = 0; tg < trigGroups.length; tg++)
{
//get the trigger names for the trigger group
String[] trigNames = scheduler.getTriggerNames(trigGroups[tg]);
for(int tn = 0; tn < trigNames.length; tn++)
{
//add information about the job to the result
jobs.add(new ScheduledJobInfo(scheduler, scheduler.getTrigger(trigNames[tn], trigGroups[tg])));
}
}
}
catch(SchedulerException se)
{
LOG.error("Failed to get scheduled jobs", se);
return null;
}
//copy the array list to a correctly typed array
Object[] oJobsArray = jobs.toArray();
ScheduledJobInfo[] jobsArray = new ScheduledJobInfo[oJobsArray.length];
System.arraycopy(oJobsArray, 0, jobsArray, 0, oJobsArray.length);
return jobsArray;
}
/**
* Gets information about currently Executing Jobs
*
* @return An array of ScheduledJobInfo
*/
public ScheduledJobInfo[] getExecutingJobs()
{
List executingJobs = null;
try
{
executingJobs = scheduler.getCurrentlyExecutingJobs();
}
catch(SchedulerException se)
{
LOG.error("Failed to get executing jobs", se);
return null;
}
ScheduledJobInfo[] jobs = new ScheduledJobInfo[executingJobs.size()];
for(int i = 0; i < executingJobs.size(); i++)
{
JobExecutionContext jec = (JobExecutionContext)executingJobs.get(i);
jobs[i] = new ScheduledJobInfo(scheduler, jec.getTrigger());
}
return jobs;
}
/**
* Set's up all the jobs that are listed in conf.xml and loaded
* through org.exist.util.Configuration
*/
public void setupConfiguredJobs()
{
Configuration.JobConfig jobList[] = (Configuration.JobConfig[])config.getProperty(Scheduler.PROPERTY_SCHEDULER_JOBS);
if(jobList == null)
return;
for(int i = 0; i < jobList.length; i ++)
{
Configuration.JobConfig jobConfig = jobList[i];
JobDescription job = null;
if(jobConfig.getResourceName().startsWith("/db/") || jobConfig.getResourceName().indexOf(':') > 0)
{
if(jobConfig.getType().equals(JOB_TYPE_SYSTEM))
{
LOG.error("System jobs may only be written in Java");
}
else
{
//create an XQuery job
User guestUser = brokerpool.getSecurityManager().getUser(SecurityManager.GUEST_USER);
job = new UserXQueryJob(jobConfig.getJobName(), jobConfig.getResourceName(), guestUser);
try {
// check if a job with the same name is already registered
if (scheduler.getJobDetail(job.getName(), UserJob.JOB_GROUP) != null) {
// yes, try to make the job's name unique
((UserXQueryJob)job).setName(job.getName() + job.hashCode());
}
} catch (SchedulerException e) {
LOG.error(e.getMessage(), e);
}
}
}
else
{
//create a Java job
try
{
Class jobClass = Class.forName(jobConfig.getResourceName());
Object jobObject = jobClass.newInstance();
if(jobConfig.getType().equals(JOB_TYPE_SYSTEM))
{
if(jobObject instanceof SystemTask)
{
SystemTask task = (SystemTask)jobObject;
task.configure(config, jobConfig.getParameters());
job = new SystemTaskJob(jobConfig.getJobName(), task);
}
else
{
LOG.error("System jobs must extend SystemTask");
}
}
else
{
job = (JobDescription)jobObject;
if (jobConfig.getJobName() != null)
job.setName(jobConfig.getJobName());
}
}
catch(Exception e)
{
LOG.error("Unable to schedule '" + jobConfig.getType() + "' job " + jobConfig.getResourceName(), e);
}
}
//if there is a job, schedule it
if(job != null)
{
if(jobConfig.getType().equals(JOB_TYPE_STARTUP))
{
//startup job - one off execution - no period, delay or repeat
createStartupJob((UserJob)job, jobConfig.getParameters());
}
else
{
//timed job
//trigger is Cron or period?
if(jobConfig.getSchedule().indexOf(' ') > -1)
{
//schedule job with Cron trigger
createCronJob(jobConfig.getSchedule(), job, jobConfig.getParameters());
}
else
{
//schedule job with periodic trigger
createPeriodicJob(Long.parseLong(jobConfig.getSchedule()), job, jobConfig.getDelay(), jobConfig.getParameters(), jobConfig.getRepeat());
}
}
}
}
}
/**
* Sets up the Job's Data Map
*
* @param job The Job
* @param jobDataMap The Job's Data Map
* @param params Any parameters for the job
*/
private void setupJobDataMap(JobDescription job, JobDataMap jobDataMap, Properties params)
{
//if this is a system job, store the BrokerPool in the job's data map
jobDataMap.put("brokerpool", brokerpool);
//if this is a system task job, store the SystemTask in the job's data map
if(job instanceof SystemTaskJob)
{
jobDataMap.put("systemtask", ((SystemTaskJob)job).getSystemTask());
}
//if this is a users XQuery job, store the XQuery resource and user in the job's data map
if(job instanceof UserXQueryJob)
{
jobDataMap.put("xqueryresource", ((UserXQueryJob)job).getXQueryResource());
jobDataMap.put("user", ((UserXQueryJob)job).getUser());
}
//copy any parameters into the job's data map
if(params != null)
{
jobDataMap.put("params", params);
}
}
}