Java tutorial
/* * Copyright 2001-2009 James House * * 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. * */ /* * Previously Copyright (c) 2001-2004 James House */ 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.opensymphony.com/quartz/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.opensymphony.com/quartz/JobSchedulingData"; public static final String QUARTZ_SCHEMA = "http://www.opensymphony.com/quartz/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); } } } }