/*
* Copyright 2001-2009 Terracotta, Inc.
*
* Licensed under the Apache 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.apache.org/licenses/LICENSE-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.quartz.xml;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.DynaProperty;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.digester.BeanPropertySetterRule;
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.RuleSetBase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobListener;
import org.quartz.ObjectAlreadyExistsException;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.spi.ClassLoadHelper;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Parses an XML file that declares Jobs and their schedules (Triggers).
*
* The xml document must conform to the format defined in
* "job_scheduling_data_1_5.dtd" or "job_scheduling_data_1_5.xsd"
*
* After creating an instance of this class, you should call one of the <code>processFile()</code>
* functions, after which you may call the <code>getScheduledJobs()</code>
* function to get a handle to the defined Jobs and Triggers, which can then be
* scheduled with the <code>Scheduler</code>. Alternatively, you could call
* the <code>processFileAndScheduleJobs()</code> function to do all of this
* in one step.
*
* The same instance can be used again and again, with the list of defined Jobs
* being cleared each time you call a <code>processFile</code> method,
* however a single instance is not thread-safe.
*
* @author <a href="mailto:bonhamcm@thirdeyeconsulting.com">Chris Bonham</a>
* @author James House
* @author pl47ypus
*/
public class JobSchedulingDataProcessor extends DefaultHandler {
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* Constants.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
public static final String QUARTZ_PUBLIC_ID = "-//Quartz Enterprise Job Scheduler//DTD Job Scheduling Data 1.5//EN";
public static final String QUARTZ_SYSTEM_ID = "http://www.quartz-scheduler.org/xml/job_scheduling_data_1_5.dtd";
public static final String QUARTZ_DTD = "/org/quartz/xml/job_scheduling_data_1_5.dtd";
public static final String QUARTZ_NS = "http://www.quartz-scheduler.org/JobSchedulingData";
public static final String QUARTZ_SCHEMA = "http://www.quartz-scheduler.org/xml/job_scheduling_data_1_5.xsd";
public static final String QUARTZ_XSD = "/org/quartz/xml/job_scheduling_data_1_5.xsd";
public static final String QUARTZ_SYSTEM_ID_DIR_PROP = "quartz.system.id.dir";
public static final String QUARTZ_XML_FILE_NAME = "quartz_jobs.xml";
public static final String QUARTZ_SYSTEM_ID_PREFIX = "jar:";
protected static final String TAG_QUARTZ = "quartz";
protected static final String TAG_OVERWRITE_EXISTING_JOBS = "overwrite-existing-jobs";
protected static final String TAG_JOB_LISTENER = "job-listener";
protected static final String TAG_CALENDAR = "calendar";
protected static final String TAG_CLASS_NAME = "class-name";
protected static final String TAG_DESCRIPTION = "description";
protected static final String TAG_BASE_CALENDAR = "base-calendar";
protected static final String TAG_MISFIRE_INSTRUCTION = "misfire-instruction";
protected static final String TAG_CALENDAR_NAME = "calendar-name";
protected static final String TAG_JOB = "job";
protected static final String TAG_JOB_DETAIL = "job-detail";
protected static final String TAG_NAME = "name";
protected static final String TAG_GROUP = "group";
protected static final String TAG_JOB_CLASS = "job-class";
protected static final String TAG_JOB_LISTENER_REF = "job-listener-ref";
protected static final String TAG_VOLATILITY = "volatility";
protected static final String TAG_DURABILITY = "durability";
protected static final String TAG_RECOVER = "recover";
protected static final String TAG_JOB_DATA_MAP = "job-data-map";
protected static final String TAG_ENTRY = "entry";
protected static final String TAG_KEY = "key";
protected static final String TAG_ALLOWS_TRANSIENT_DATA = "allows-transient-data";
protected static final String TAG_VALUE = "value";
protected static final String TAG_TRIGGER = "trigger";
protected static final String TAG_SIMPLE = "simple";
protected static final String TAG_CRON = "cron";
protected static final String TAG_JOB_NAME = "job-name";
protected static final String TAG_JOB_GROUP = "job-group";
protected static final String TAG_START_TIME = "start-time";
protected static final String TAG_END_TIME = "end-time";
protected static final String TAG_REPEAT_COUNT = "repeat-count";
protected static final String TAG_REPEAT_INTERVAL = "repeat-interval";
protected static final String TAG_CRON_EXPRESSION = "cron-expression";
protected static final String TAG_TIME_ZONE = "time-zone";
/**
* XML Schema dateTime datatype format.
* <p>
* See <a href="http://www.w3.org/TR/2001/REC-xmlschema-2-20010502/#dateTime">
* http://www.w3.org/TR/2001/REC-xmlschema-2-20010502/#dateTime</a>
*/
protected static final String XSD_DATE_FORMAT = "yyyy-MM-dd'T'hh:mm:ss";
/**
* Legacy DTD version 1.0 date format.
*/
protected static final String DTD_DATE_FORMAT = "yyyy-MM-dd hh:mm:ss a";
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* Data members.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
protected Map scheduledJobs = new HashMap();
protected List jobsToSchedule = new LinkedList();
protected List calsToSchedule = new LinkedList();
protected List listenersToSchedule = new LinkedList();
protected Collection validationExceptions = new ArrayList();
protected ClassLoadHelper classLoadHelper;
protected Digester digester;
private boolean overWriteExistingJobs = true;
private ThreadLocal schedLocal = new ThreadLocal();
private final Log log = LogFactory.getLog(getClass());
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* Constructors.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
/**
* Constructor for QuartzMetaDataProcessor.
*/
private JobSchedulingDataProcessor() {
// Hidden, null implementation.
}
/**
* Constructor for QuartzMetaDataProcessor.
*
* @param clh class-loader helper to share with digester.
* @param validating whether or not to validate XML.
* @param validatingSchema whether or not to validate XML schema.
*/
public JobSchedulingDataProcessor(ClassLoadHelper clh, boolean validating, boolean validatingSchema) {
this.classLoadHelper = clh;
initDigester(validating, validatingSchema);
}
/**
* Initializes the digester.
*
* @param validating whether or not to validate XML.
* @param validatingSchema whether or not to validate XML schema.
*/
protected void initDigester(boolean validating, boolean validatingSchema) {
digester = new Digester();
digester.setNamespaceAware(true);
digester.setClassLoader(this.classLoadHelper.getClassLoader());
digester.setValidating(validating);
initSchemaValidation(validatingSchema);
digester.setEntityResolver(this);
digester.setErrorHandler(this);
if (addCustomDigesterRules(digester)) {
addDefaultDigesterRules(digester);
}
}
/**
* Add the default set of digest rules
*/
protected void addDefaultDigesterRules(Digester digester) {
digester.addSetProperties(TAG_QUARTZ, TAG_OVERWRITE_EXISTING_JOBS, "overWriteExistingJobs");
digester.addObjectCreate(TAG_QUARTZ + "/" + TAG_JOB_LISTENER, "jobListener","class-name");
digester.addCallMethod(TAG_QUARTZ + "/" + TAG_JOB_LISTENER,"setName",1);
digester.addCallParam(TAG_QUARTZ + "/" + TAG_JOB_LISTENER,0,"name");
digester.addSetNext(TAG_QUARTZ + "/" + TAG_JOB_LISTENER,"addListenerToSchedule");
digester.addRuleSet(new CalendarRuleSet(TAG_QUARTZ + "/" + TAG_CALENDAR, "addCalendarToSchedule"));
digester.addRuleSet(new CalendarRuleSet("*/" + TAG_BASE_CALENDAR, "setBaseCalendar"));
digester.addObjectCreate(TAG_QUARTZ + "/" + TAG_JOB, JobSchedulingBundle.class);
digester.addObjectCreate(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL, JobDetail.class);
digester.addBeanPropertySetter(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_NAME, "name");
digester.addBeanPropertySetter(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_GROUP, "group");
digester.addBeanPropertySetter(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_DESCRIPTION, "description");
digester.addBeanPropertySetter(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_JOB_CLASS, "jobClass");
digester.addCallMethod(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_JOB_LISTENER_REF,"addJobListener",0 );
digester.addBeanPropertySetter(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_VOLATILITY, "volatility");
digester.addBeanPropertySetter(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_DURABILITY, "durability");
digester.addBeanPropertySetter(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_RECOVER, "requestsRecovery");
digester.addObjectCreate(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_JOB_DATA_MAP, JobDataMap.class);
digester.addCallMethod(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_JOB_DATA_MAP + "/" + TAG_ENTRY, "put", 2, new Class[] { Object.class, Object.class });
digester.addCallParam(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_JOB_DATA_MAP + "/" + TAG_ENTRY + "/" + TAG_KEY, 0);
digester.addCallParam(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_JOB_DATA_MAP + "/" + TAG_ENTRY + "/" + TAG_VALUE, 1);
digester.addSetNext(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL + "/" + TAG_JOB_DATA_MAP, "setJobDataMap");
digester.addSetNext(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_JOB_DETAIL, "setJobDetail");
digester.addRuleSet(new TriggerRuleSet(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_TRIGGER + "/" + TAG_SIMPLE, SimpleTrigger.class));
digester.addBeanPropertySetter(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_TRIGGER + "/" + TAG_SIMPLE + "/" + TAG_REPEAT_COUNT, "repeatCount");
digester.addBeanPropertySetter(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_TRIGGER + "/" + TAG_SIMPLE + "/" + TAG_REPEAT_INTERVAL, "repeatInterval");
digester.addSetNext(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_TRIGGER + "/" + TAG_SIMPLE, "addTrigger");
digester.addRuleSet(new TriggerRuleSet(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_TRIGGER + "/" + TAG_CRON, CronTrigger.class));
digester.addBeanPropertySetter(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_TRIGGER + "/" + TAG_CRON + "/" + TAG_CRON_EXPRESSION, "cronExpression");
digester.addRule(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_TRIGGER + "/" + TAG_CRON + "/" + TAG_TIME_ZONE, new SimpleConverterRule("timeZone", new TimeZoneConverter(), TimeZone.class));
digester.addSetNext(TAG_QUARTZ + "/" + TAG_JOB + "/" + TAG_TRIGGER + "/" + TAG_CRON, "addTrigger");
digester.addSetNext(TAG_QUARTZ + "/" + TAG_JOB, "addJobToSchedule");
}
/**
* Template method provided as a hook for those who wish to extend this
* class and add more functionality.
*
* This method is invoked after the Digester is instantiated, and before
* the default set of rules are added.
*
* @param digester
* @return false, if the default rules should NOT be added
*/
protected boolean addCustomDigesterRules(Digester digester) {
// do nothing in base impl
return true;
}
/**
* Initializes the digester for XML Schema validation.
*
* @param validatingSchema whether or not to validate XML.
*/
protected void initSchemaValidation(boolean validatingSchema) {
if (validatingSchema) {
String schemaUri = null;
URL url = getClass().getResource(QUARTZ_XSD);
if (url != null) {
schemaUri = url.toExternalForm();
} else {
schemaUri = QUARTZ_SCHEMA;
}
digester.setSchema(schemaUri);
}
}
protected Log getLog() {
return log;
}
/**
* Returns whether to use the context class loader.
*
* @return whether to use the context class loader.
*/
public boolean getUseContextClassLoader() {
return digester.getUseContextClassLoader();
}
/**
* Sets whether to use the context class loader.
*
* @param useContextClassLoader boolean.
*/
public void setUseContextClassLoader(boolean useContextClassLoader) {
digester.setUseContextClassLoader(useContextClassLoader);
}
/**
* Returns whether to overwrite existing jobs.
*
* @return whether to overwrite existing jobs.
*/
public boolean getOverWriteExistingJobs() {
return overWriteExistingJobs;
}
/**
* Sets whether to overwrite existing jobs.
*
* @param overWriteExistingJobs boolean.
*/
public void setOverWriteExistingJobs(boolean overWriteExistingJobs) {
this.overWriteExistingJobs = overWriteExistingJobs;
}
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* Interface.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
/**
* Process the xml file in the default location (a file named
* "quartz_jobs.xml" in the current working directory).
*
*/
public void processFile() throws Exception {
processFile(QUARTZ_XML_FILE_NAME);
}
/**
* Process the xml file named <code>fileName</code>.
*
* @param fileName
* meta data file name.
*/
public void processFile(String fileName) throws Exception {
processFile(fileName, getSystemIdForFileName(fileName));
}
/**
* For the given <code>fileName</code>, attempt to expand it to its full path
* for use as a system id.
*
* @see #getURL(String)
* @see #processFile()
* @see #processFile(String)
* @see #processFileAndScheduleJobs(Scheduler, boolean)
* @see #processFileAndScheduleJobs(String, Scheduler, boolean)
*/
protected String getSystemIdForFileName(String fileName) {
InputStream fileInputStream = null;
try {
String urlPath = null;
File file = new File(fileName); // files in filesystem
if (!file.exists()) {
URL url = getURL(fileName);
if (url != null) {
// Required for jdk 1.3 compatibility
urlPath = URLDecoder.decode(url.getPath());
try {
fileInputStream = url.openStream();
} catch (IOException ignore) {
}
}
} else {
try {
fileInputStream = new FileInputStream(file);
}catch (FileNotFoundException ignore) {
}
}
if (fileInputStream == null) {
getLog().debug("Unable to resolve '" + fileName + "' to full path, so using it as is for system id.");
return fileName;
} else {
return (urlPath != null) ? urlPath : file.getAbsolutePath();
}
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException ioe) {
getLog().warn("Error closing jobs file: " + fileName, ioe);
}
}
}
/**
* Returns an <code>URL</code> from the fileName as a resource.
*
* @param fileName
* file name.
* @return an <code>URL</code> from the fileName as a resource.
*/
protected URL getURL(String fileName) {
return Thread.currentThread().getContextClassLoader().getResource(fileName);
}
/**
* Process the xmlfile named <code>fileName</code> with the given system
* ID.
*
* @param fileName
* meta data file name.
* @param systemId
* system ID.
*/
public void processFile(String fileName, String systemId)
throws ValidationException, ParserConfigurationException,
SAXException, IOException, SchedulerException,
ClassNotFoundException, ParseException {
clearValidationExceptions();
scheduledJobs.clear();
jobsToSchedule.clear();
calsToSchedule.clear();
getLog().info("Parsing XML file: " + fileName +
" with systemId: " + systemId +
" validating: " + digester.getValidating() +
" validating schema: " + digester.getSchema());
InputSource is = new InputSource(getInputStream(fileName));
is.setSystemId(systemId);
digester.push(this);
digester.parse(is);
maybeThrowValidationException();
}
/**
* Process the xmlfile named <code>fileName</code> with the given system
* ID.
*
* @param stream
* an input stream containing the xml content.
* @param systemId
* system ID.
*/
public void processStream(InputStream stream, String systemId)
throws ValidationException, ParserConfigurationException,
SAXException, IOException, SchedulerException,
ClassNotFoundException, ParseException {
clearValidationExceptions();
scheduledJobs.clear();
jobsToSchedule.clear();
calsToSchedule.clear();
getLog().info("Parsing XML from stream with systemId: " + systemId +
" validating: " + digester.getValidating() +
" validating schema: " + digester.getSchema());
InputSource is = new InputSource(stream);
is.setSystemId(systemId);
digester.push(this);
digester.parse(is);
maybeThrowValidationException();
}
/**
* Process the xml file in the default location, and schedule all of the
* jobs defined within it.
*
*/
public void processFileAndScheduleJobs(Scheduler sched,
boolean overWriteExistingJobs) throws SchedulerException, Exception {
processFileAndScheduleJobs(QUARTZ_XML_FILE_NAME, sched,
overWriteExistingJobs);
}
/**
* Process the xml file in the given location, and schedule all of the
* jobs defined within it.
*
* @param fileName
* meta data file name.
*/
public void processFileAndScheduleJobs(String fileName, Scheduler sched,
boolean overWriteExistingJobs) throws Exception {
processFileAndScheduleJobs(fileName, getSystemIdForFileName(fileName), sched, overWriteExistingJobs);
}
/**
* Process the xml file in the given location, and schedule all of the
* jobs defined within it.
*
* @param fileName
* meta data file name.
*/
public void processFileAndScheduleJobs(String fileName, String systemId,
Scheduler sched, boolean overWriteExistingJobs) throws Exception {
schedLocal.set(sched);
try {
processFile(fileName, systemId);
scheduleJobs(getScheduledJobs(), sched, overWriteExistingJobs);
} finally {
schedLocal.set(null);
}
}
/**
* Add the Jobs and Triggers defined in the given map of <code>JobSchedulingBundle</code>
* s to the given scheduler.
*
* @param jobBundles
* @param sched
* @param overWriteExistingJobs
* @throws Exception
*/
public void scheduleJobs(Map jobBundles, Scheduler sched,
boolean overWriteExistingJobs) throws Exception {
getLog().info("Scheduling " + jobsToSchedule.size() + " parsed jobs.");
Iterator itr = calsToSchedule.iterator();
while (itr.hasNext()) {
CalendarBundle bndle = (CalendarBundle) itr.next();
addCalendar(sched, bndle);
}
itr = jobsToSchedule.iterator();
while (itr.hasNext()) {
JobSchedulingBundle bndle = (JobSchedulingBundle) itr.next();
scheduleJob(bndle, sched, overWriteExistingJobs);
}
itr = listenersToSchedule.iterator();
while (itr.hasNext()) {
JobListener listener = (JobListener) itr.next();
getLog().info("adding listener "+listener.getName()+" of class "+listener.getClass().getName());
sched.addJobListener(listener);
}
getLog().info(jobBundles.size() + " scheduled jobs.");
}
/**
* Returns a <code>Map</code> of scheduled jobs.
* <p/>
* The key is the job name and the value is a <code>JobSchedulingBundle</code>
* containing the <code>JobDetail</code> and <code>Trigger</code>.
*
* @return a <code>Map</code> of scheduled jobs.
*/
public Map getScheduledJobs() {
return Collections.unmodifiableMap(scheduledJobs);
}
/**
* Returns a <code>JobSchedulingBundle</code> for the job name.
*
* @param name
* job name.
* @return a <code>JobSchedulingBundle</code> for the job name.
*/
public JobSchedulingBundle getScheduledJob(String name) {
return (JobSchedulingBundle) getScheduledJobs().get(name);
}
/**
* Returns an <code>InputStream</code> from the fileName as a resource.
*
* @param fileName
* file name.
* @return an <code>InputStream</code> from the fileName as a resource.
*/
protected InputStream getInputStream(String fileName) {
return this.classLoadHelper.getResourceAsStream(fileName);
}
/**
* Schedules a given job and trigger (both wrapped by a <code>JobSchedulingBundle</code>).
*
* @param job
* job wrapper.
* @exception SchedulerException
* if the Job or Trigger cannot be added to the Scheduler, or
* there is an internal Scheduler error.
*/
public void scheduleJob(JobSchedulingBundle job)
throws SchedulerException {
scheduleJob(job, (Scheduler) schedLocal.get(), getOverWriteExistingJobs());
}
public void addJobToSchedule(JobSchedulingBundle job) {
jobsToSchedule.add(job);
}
public void addCalendarToSchedule(CalendarBundle cal) {
calsToSchedule.add(cal);
}
public void addListenerToSchedule(JobListener listener) {
listenersToSchedule.add(listener);
}
/**
* Schedules a given job and trigger (both wrapped by a <code>JobSchedulingBundle</code>).
*
* @param job
* job wrapper.
* @param sched
* job scheduler.
* @param localOverWriteExistingJobs
* locally overwrite existing jobs.
* @exception SchedulerException
* if the Job or Trigger cannot be added to the Scheduler, or
* there is an internal Scheduler error.
*/
public void scheduleJob(JobSchedulingBundle job, Scheduler sched, boolean localOverWriteExistingJobs)
throws SchedulerException {
if ((job != null) && job.isValid()) {
JobDetail detail = job.getJobDetail();
JobDetail dupeJ = sched.getJobDetail(detail.getName(), detail.getGroup());
if ((dupeJ != null) && !localOverWriteExistingJobs) {
getLog().info("Not overwriting existing job: " + dupeJ.getFullName());
return;
}
if (dupeJ != null) {
getLog().info("Replacing job: " + detail.getFullName());
} else {
getLog().info("Adding job: " + detail.getFullName());
}
if (job.getTriggers().size() == 0 && !job.getJobDetail().isDurable()) {
if (dupeJ == null) {
throw new SchedulerException(
"A new job defined without any triggers must be durable: " +
detail.getFullName());
}
if ((dupeJ.isDurable() &&
(sched.getTriggersOfJob(
detail.getName(), detail.getGroup()).length == 0))) {
throw new SchedulerException(
"Can't make a durable job without triggers non-durable: " +
detail.getFullName());
}
}
sched.addJob(detail, true);
for (Iterator iter = job.getTriggers().iterator(); iter.hasNext(); ) {
Trigger trigger = (Trigger)iter.next();
trigger.setJobName(detail.getName());
trigger.setJobGroup(detail.getGroup());
if(trigger.getStartTime() == null) {
trigger.setStartTime(new Date());
}
boolean addedTrigger = false;
while (addedTrigger == false) {
Trigger dupeT = sched.getTrigger(trigger.getName(), trigger.getGroup());
if (dupeT != null) {
if (getLog().isDebugEnabled()) {
getLog().debug(
"Rescheduling job: " + detail.getFullName() + " with updated trigger: " + trigger.getFullName());
}
if(!dupeT.getJobGroup().equals(trigger.getJobGroup()) || !dupeT.getJobName().equals(trigger.getJobName())) {
getLog().warn("Possibly duplicately named triggers in jobs xml file!");
}
sched.rescheduleJob(trigger.getName(), trigger.getGroup(), trigger);
} else {
if (getLog().isDebugEnabled()) {
getLog().debug(
"Scheduling job: " + detail.getFullName() + " with trigger: " + trigger.getFullName());
}
try {
sched.scheduleJob(trigger);
} catch (ObjectAlreadyExistsException e) {
if (getLog().isDebugEnabled()) {
getLog().debug(
"Adding trigger: " + trigger.getFullName() + " for job: " + detail.getFullName() +
" failed because the trigger already existed. " +
"This is likely due to a race condition between multiple instances " +
"in the cluster. Will try to reschedule instead.");
}
continue;
}
}
addedTrigger = true;
}
}
addScheduledJob(job);
}
}
/**
* Adds a scheduled job.
*
* @param job
* job wrapper.
*/
protected void addScheduledJob(JobSchedulingBundle job) {
scheduledJobs.put(job.getFullName(), job);
}
/**
* Adds a calendar.
*
* @param calendarBundle calendar bundle.
* @throws SchedulerException if the Calendar cannot be added to the Scheduler, or
* there is an internal Scheduler error.
*/
public void addCalendar(Scheduler sched, CalendarBundle calendarBundle) throws SchedulerException {
sched.addCalendar(
calendarBundle.getCalendarName(),
calendarBundle.getCalendar(),
calendarBundle.getReplace(),
true);
}
/**
* EntityResolver interface.
* <p/>
* Allow the application to resolve external entities.
* <p/>
* Until <code>quartz.dtd</code> has a public ID, it must resolved as a
* system ID. Here's the order of resolution (if one fails, continue to the
* next).
* <ol>
* <li>Tries to resolve the <code>systemId</code> with <code>ClassLoader.getResourceAsStream(String)</code>.
* </li>
* <li>If the <code>systemId</code> starts with <code>QUARTZ_SYSTEM_ID_PREFIX</code>,
* then resolve the part after <code>QUARTZ_SYSTEM_ID_PREFIX</code> with
* <code>ClassLoader.getResourceAsStream(String)</code>.</li>
* <li>Else try to resolve <code>systemId</code> as a URL.
* <li>If <code>systemId</code> has a colon in it, create a new <code>URL</code>
* </li>
* <li>Else resolve <code>systemId</code> as a <code>File</code> and
* then call <code>File.toURL()</code>.</li>
* </li>
* </ol>
* <p/>
* If the <code>publicId</code> does exist, resolve it as a URL. If the
* <code>publicId</code> is the Quartz public ID, then resolve it locally.
*
* @param publicId
* The public identifier of the external entity being referenced,
* or null if none was supplied.
* @param systemId
* The system identifier of the external entity being referenced.
* @return An InputSource object describing the new input source, or null
* to request that the parser open a regular URI connection to the
* system identifier.
* @exception SAXException
* Any SAX exception, possibly wrapping another exception.
* @exception IOException
* A Java-specific IO exception, possibly the result of
* creating a new InputStream or Reader for the InputSource.
*/
public InputSource resolveEntity(String publicId, String systemId) {
InputSource inputSource = null;
InputStream is = null;
URL url = null;
try {
if (publicId == null) {
if (systemId != null) {
// resolve Quartz Schema locally
if (QUARTZ_SCHEMA.equals(systemId)) {
is = getClass().getResourceAsStream(QUARTZ_XSD);
} else {
is = getInputStream(systemId);
if (is == null) {
int start = systemId.indexOf(QUARTZ_SYSTEM_ID_PREFIX);
if (start > -1) {
String fileName = systemId
.substring(QUARTZ_SYSTEM_ID_PREFIX.length());
is = getInputStream(fileName);
} else {
if (systemId.indexOf(':') == -1) {
File file = new java.io.File(systemId);
url = file.toURL();
} else {
url = new URL(systemId);
}
is = url.openStream();
}
}
}
}
} else {
// resolve Quartz DTD locally
if (QUARTZ_PUBLIC_ID.equals(publicId)) {
is = getClass().getResourceAsStream(QUARTZ_DTD);
} else {
url = new URL(systemId);
is = url.openStream();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
inputSource = new InputSource(is);
inputSource.setPublicId(publicId);
inputSource.setSystemId(systemId);
}
}
return inputSource;
}
/**
* ErrorHandler interface.
*
* Receive notification of a warning.
*
* @param e
* The error information encapsulated in a SAX parse exception.
* @exception SAXException
* Any SAX exception, possibly wrapping another exception.
*/
public void warning(SAXParseException e) throws SAXException {
addValidationException(e);
}
/**
* ErrorHandler interface.
*
* Receive notification of a recoverable error.
*
* @param e
* The error information encapsulated in a SAX parse exception.
* @exception SAXException
* Any SAX exception, possibly wrapping another exception.
*/
public void error(SAXParseException e) throws SAXException {
addValidationException(e);
}
/**
* ErrorHandler interface.
*
* Receive notification of a non-recoverable error.
*
* @param e
* The error information encapsulated in a SAX parse exception.
* @exception SAXException
* Any SAX exception, possibly wrapping another exception.
*/
public void fatalError(SAXParseException e) throws SAXException {
addValidationException(e);
}
/**
* Adds a detected validation exception.
*
* @param e
* SAX exception.
*/
protected void addValidationException(SAXException e) {
validationExceptions.add(e);
}
/**
* Resets the the number of detected validation exceptions.
*/
protected void clearValidationExceptions() {
validationExceptions.clear();
}
/**
* Throws a ValidationException if the number of validationExceptions
* detected is greater than zero.
*
* @exception ValidationException
* DTD validation exception.
*/
protected void maybeThrowValidationException() throws ValidationException {
if (validationExceptions.size() > 0) {
throw new ValidationException(validationExceptions);
}
}
/**
* RuleSet for common Calendar tags.
*
* @author <a href="mailto:bonhamcm@thirdeyeconsulting.com">Chris Bonham</a>
*/
public class CalendarRuleSet extends RuleSetBase {
protected String prefix;
protected String setNextMethodName;
public CalendarRuleSet(String prefix, String setNextMethodName) {
super();
this.prefix = prefix;
this.setNextMethodName = setNextMethodName;
}
public void addRuleInstances(Digester digester) {
digester.addObjectCreate(prefix, CalendarBundle.class);
digester.addSetProperties(prefix, TAG_CLASS_NAME, "className");
digester.addBeanPropertySetter(prefix + "/" + TAG_NAME, "calendarName");
digester.addBeanPropertySetter(prefix + "/" + TAG_DESCRIPTION, "description");
digester.addSetNext(prefix, setNextMethodName);
}
}
/**
* RuleSet for common Trigger tags.
*
* @author <a href="mailto:bonhamcm@thirdeyeconsulting.com">Chris Bonham</a>
*/
public class TriggerRuleSet extends RuleSetBase {
protected String prefix;
protected Class clazz;
public TriggerRuleSet(String prefix, Class clazz) {
super();
this.prefix = prefix;
if (!Trigger.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Class must be an instance of Trigger");
}
this.clazz = clazz;
}
public void addRuleInstances(Digester digester) {
digester.addObjectCreate(prefix, clazz);
digester.addBeanPropertySetter(prefix + "/" + TAG_NAME, "name");
digester.addBeanPropertySetter(prefix + "/" + TAG_GROUP, "group");
digester.addBeanPropertySetter(prefix + "/" + TAG_DESCRIPTION, "description");
digester.addBeanPropertySetter(prefix + "/" + TAG_VOLATILITY, "volatility");
digester.addRule(prefix + "/" + TAG_MISFIRE_INSTRUCTION, new MisfireInstructionRule("misfireInstruction"));
digester.addBeanPropertySetter(prefix + "/" + TAG_CALENDAR_NAME, "calendarName");
digester.addObjectCreate(prefix + "/" + TAG_JOB_DATA_MAP, JobDataMap.class);
digester.addCallMethod(prefix + "/" + TAG_JOB_DATA_MAP + "/" + TAG_ENTRY, "put", 2, new Class[] { Object.class, Object.class });
digester.addCallParam(prefix + "/" + TAG_JOB_DATA_MAP + "/" + TAG_ENTRY + "/" + TAG_KEY, 0);
digester.addCallParam(prefix + "/" + TAG_JOB_DATA_MAP + "/" + TAG_ENTRY + "/" + TAG_VALUE, 1);
digester.addSetNext(prefix + "/" + TAG_JOB_DATA_MAP, "setJobDataMap");
digester.addBeanPropertySetter(prefix + "/" + TAG_JOB_NAME, "jobName");
digester.addBeanPropertySetter(prefix + "/" + TAG_JOB_GROUP, "jobGroup");
Converter converter = new DateConverter(new String[] { XSD_DATE_FORMAT, DTD_DATE_FORMAT });
digester.addRule(prefix + "/" + TAG_START_TIME, new SimpleConverterRule("startTime", converter, Date.class));
digester.addRule(prefix + "/" + TAG_END_TIME, new SimpleConverterRule("endTime", converter, Date.class));
}
}
/**
* This rule is needed to fix <a href="http://jira.opensymphony.com/browse/QUARTZ-153">QUARTZ-153</a>.
* <p>
* Since the Jakarta Commons BeanUtils 1.6.x <code>ConvertUtils</code> class uses static utility
* methods, the <code>DateConverter</code> and <code>TimeZoneConverter</code> were
* overriding any previously registered converters for <code>java.util.Date</code> and
* <code>java.util.TimeZone</code>.
* <p>
* Jakarta Commons BeanUtils 1.7.x fixes this issue by internally using per-context-classloader
* pseudo-singletons (see <a href="http://jakarta.apache.org/commons/beanutils/commons-beanutils-1.7.0/RELEASE-NOTES.txt">
* http://jakarta.apache.org/commons/beanutils/commons-beanutils-1.7.0/RELEASE-NOTES.txt</a>).
* This ensures web applications in the same JVM are using independent converters
* based on their classloaders. However, the environment for QUARTZ-153 started Quartz
* using the <code>QuartzInitializationServlet</code> which started <code>JobInitializationPlugin</code>.
* In this case, the web classloader instances would be the same.
* <p>
* To make sure the converters aren't overridden by the <code>JobSchedulingDataProcessor</code>,
* it's easier to just override <code>BeanPropertySetterRule.end()</code> to convert the
* body text to the specified class using the specified converter.
*
* @author <a href="mailto:bonhamcm@thirdeyeconsulting.com">Chris Bonham</a>
*/
public class SimpleConverterRule extends BeanPropertySetterRule {
private Converter converter;
private Class clazz;
/**
* <p>Construct rule that sets the given property from the body text.</p>
*
* @param propertyName name of property to set
* @param converter converter to use
* @param clazz class to convert to
*/
public SimpleConverterRule(String propertyName, Converter converter, Class clazz) {
this.propertyName = propertyName;
if (converter == null) {
throw new IllegalArgumentException("Converter must not be null");
}
this.converter = converter;
if (clazz == null) {
throw new IllegalArgumentException("Class must not be null");
}
this.clazz = clazz;
}
/**
* Process the end of this element.
*
* @param namespace the namespace URI of the matching element, or an
* empty string if the parser is not namespace aware or the element has
* no namespace
* @param name the local name if the parser is namespace aware, or just
* the element name otherwise
*
* @exception NoSuchMethodException if the bean does not
* have a writeable property of the specified name
*/
public void end(String namespace, String name) throws Exception {
String property = propertyName;
if (property == null) {
// If we don't have a specific property name,
// use the element name.
property = name;
}
// Get a reference to the top object
Object top = this.digester.peek();
// log some debugging information
if (getDigester().getLogger().isDebugEnabled()) {
getDigester().getLogger().debug("[BeanPropertySetterRule]{" + getDigester().getMatch() +
"} Set " + top.getClass().getName() + " property " +
property + " with text " + bodyText);
}
// Force an exception if the property does not exist
// (BeanUtils.setProperty() silently returns in this case)
if (top instanceof DynaBean) {
DynaProperty desc =
((DynaBean) top).getDynaClass().getDynaProperty(property);
if (desc == null) {
throw new NoSuchMethodException ("Bean has no property named " + property);
}
} else /* this is a standard JavaBean */ {
PropertyDescriptor desc =
PropertyUtils.getPropertyDescriptor(top, property);
if (desc == null) {
throw new NoSuchMethodException("Bean has no property named " + property);
}
}
// Set the property only using this converter
Object value = converter.convert(clazz, bodyText);
PropertyUtils.setProperty(top, property, value);
}
}
/**
* This rule translates the trigger misfire instruction constant name into its
* corresponding value.
*
* <p>
* TODO Consider removing this class and using a
* <code>org.apache.commons.digester.Substitutor</code> strategy once
* Jakarta Commons Digester 1.6 is final.
* </p>
*
* @author <a href="mailto:bonhamcm@thirdeyeconsulting.com">Chris Bonham</a>
*/
public class MisfireInstructionRule extends BeanPropertySetterRule {
/**
* <p>Construct rule that sets the given property from the body text.</p>
*
* @param propertyName name of property to set
*/
public MisfireInstructionRule(String propertyName) {
this.propertyName = propertyName;
}
/**
* Process the body text of this element.
*
* @param namespace the namespace URI of the matching element, or an
* empty string if the parser is not namespace aware or the element has
* no namespace
* @param name the local name if the parser is namespace aware, or just
* the element name otherwise
* @param text The text of the body of this element
*/
public void body(String namespace, String name, String text)
throws Exception {
super.body(namespace, name, text);
this.bodyText = getConstantValue(bodyText);
}
/**
* Returns the value for the constant name.
* If the constant can't be found or any exceptions occur,
* return 0.
*
* @param constantName constant name.
* @return the value for the constant name.
*/
private String getConstantValue(String constantName) {
String value = "0";
Object top = this.digester.peek();
if (top != null) {
Class clazz = top.getClass();
try {
java.lang.reflect.Field field = clazz.getField(constantName);
Object fieldValue = field.get(top);
if (fieldValue != null) {
value = fieldValue.toString();
}
} catch (Exception e) {
// ignore
}
}
return value;
}
}
/**
* <p>Standard {@link Converter} implementation that converts an incoming
* String into a <code>java.util.Date</code> object, optionally using a
* default value or throwing a {@link ConversionException} if a conversion
* error occurs.</p>
*/
public final class DateConverter implements Converter {
// ----------------------------------------------------------- Constructors
/**
* Create a {@link Converter} that will throw a {@link ConversionException}
* if a conversion error occurs.
*/
public DateConverter() {
this.defaultValue = null;
this.useDefault = false;
}
/**
* Create a {@link Converter} that will return the specified default value
* if a conversion error occurs.
*
* @param defaultValue The default value to be returned
*/
public DateConverter(Object defaultValue) {
this.defaultValue = defaultValue;
this.useDefault = true;
}
public DateConverter(String[] formats) {
this();
int len = formats.length;
dateFormats = new DateFormat[len];
for (int i = 0; i < len; i++) {
dateFormats[i] = new SimpleDateFormat(formats[i]);
}
}
// ----------------------------------------------------- Instance Variables
/**
* The default value specified to our Constructor, if any.
*/
private Object defaultValue = null;
/**
* Should we return the default value on conversion errors?
*/
private boolean useDefault = true;
private DateFormat[] dateFormats;
// --------------------------------------------------------- Public Methods
/**
* Convert the specified input object into an output object of the
* specified type.
*
* @param type Data type to which this value should be converted
* @param value The input value to be converted
*
* @exception ConversionException if conversion cannot be performed
* successfully
*/
public Object convert(Class type, Object value) {
if (value == null) {
if (useDefault) {
return (defaultValue);
} else {
return (null);
}
}
if (String.class.equals(type)) {
if ((value instanceof Date) && (dateFormats != null)) {
return (dateFormats[0].format((Date) value));
} else {
return (value.toString());
}
}
if (value instanceof Date) {
return (value);
}
try {
if (Date.class.isAssignableFrom(type) && dateFormats != null) {
return parseDate(value);
} else {
return (value.toString());
}
} catch (Exception e) {
if (useDefault) {
return (defaultValue);
} else {
throw new ConversionException(e);
}
}
}
protected Date parseDate(Object value) throws ParseException {
Date date = null;
int len = dateFormats.length;
for (int i = 0; i < len; i++) {
try {
date = (dateFormats[i].parse(value.toString()));
break;
} catch (ParseException e) {
// if this is the last format, throw the exception
if (i == (len - 1)) {
throw e;
}
}
}
return date;
}
}
/**
* <p>Standard {@link Converter} implementation that converts an incoming
* String into a <code>java.util.TimeZone</code> object throwing a
* {@link ConversionException} if a conversion error occurs.</p>
*/
public final class TimeZoneConverter implements Converter {
// ----------------------------------------------------------- Constructors
/**
* Create a {@link Converter} that will throw a {@link ConversionException}
* if a conversion error occurs.
*/
public TimeZoneConverter() {
}
// --------------------------------------------------------- Public Methods
/**
* Convert the specified input object into an output object of the
* specified type.
*
* @param type Data type to which this value should be converted
* @param value The input value to be converted
*
* @exception ConversionException if conversion cannot be performed
* successfully
*/
public Object convert(Class type, Object value) {
if (value == null) {
return (null);
}
if (value instanceof TimeZone) {
return (value);
}
try {
if (String.class.equals(value.getClass())) {
return (TimeZone.getTimeZone((String) value));
} else {
return (value.toString());
}
} catch (Exception e) {
throw new ConversionException(e);
}
}
}
}