All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.quartz.plugins.xml.XMLSchedulingDataProcessor Maven / Gradle / Ivy

/**
 * Copyright 2001-2010 Terracotta, Inc.
 * Copyright 2011 Xeiam LLC
 *
 * 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.plugins.xml;

import static org.quartz.builders.CronTriggerBuilder.cronTriggerBuilder;
import static org.quartz.builders.JobBuilder.newJobBuilder;
import static org.quartz.builders.SimpleTriggerBuilder.simpleTriggerBuilder;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
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.LinkedList;
import java.util.List;
import java.util.TimeZone;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.quartz.builders.CronTriggerBuilder;
import org.quartz.builders.SimpleTriggerBuilder;
import org.quartz.classloading.ClassLoadHelper;
import org.quartz.core.Scheduler;
import org.quartz.exceptions.ObjectAlreadyExistsException;
import org.quartz.exceptions.SchedulerException;
import org.quartz.jobs.JobDetail;
import org.quartz.triggers.OperableTrigger;
import org.quartz.triggers.SimpleTrigger;
import org.quartz.triggers.Trigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import com.xeiam.sundial.Job;

/**
 * Parses an XML file that declares Jobs and their schedules (Triggers), and processes the related data.
 *
 * @author James House
 * @author Past contributions from Chris Bonham
 * @author Past contributions from pl47ypus
 * @author timmolter
 * @since Quartz 1.8
 */
public class XMLSchedulingDataProcessor implements ErrorHandler {

  /*
   * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Constants.
   * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   */

  private static final String QUARTZ_XSD_PATH_IN_JAR = "com/xeiam/sundial/xml/job_scheduling_data.xsd";

  public static final String QUARTZ_XML_DEFAULT_FILE_NAME = "jobs.xml";

  /**
   * XML Schema dateTime datatype format.
   * 

* See http://www.w3.org/TR/2001/REC-xmlschema-2-20010502/#dateTime */ private static final String XSD_DATE_FORMAT = "yyyy-MM-dd'T'hh:mm:ss"; private static final SimpleDateFormat dateFormat = new SimpleDateFormat(XSD_DATE_FORMAT); /* * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Data members. * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ // scheduling commands private List loadedJobs = new LinkedList(); private List loadedTriggers = new LinkedList(); private Collection validationExceptions = new ArrayList(); private ClassLoadHelper classLoadHelper = null; private DocumentBuilder docBuilder = null; private XPath xpath = null; private final Logger logger = LoggerFactory.getLogger(XMLSchedulingDataProcessor.class); /* * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Constructors. * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /** * Constructor for JobSchedulingDataLoader. * * @param classLoadHelper * @param clh class-loader helper to share with digester. * @throws ParserConfigurationException if the XML parser cannot be configured as needed. */ public XMLSchedulingDataProcessor(ClassLoadHelper classLoadHelper) throws ParserConfigurationException { this.classLoadHelper = classLoadHelper; initDocumentParser(); } /** * Initializes the XML parser. * * @throws ParserConfigurationException */ private void initDocumentParser() throws ParserConfigurationException { DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); docBuilderFactory.setNamespaceAware(false); docBuilderFactory.setValidating(true); docBuilderFactory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema"); docBuilderFactory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaSource", resolveSchemaSource()); docBuilder = docBuilderFactory.newDocumentBuilder(); docBuilder.setErrorHandler(this); xpath = XPathFactory.newInstance().newXPath(); } private Object resolveSchemaSource() { InputSource inputSource = null; InputStream is = null; // try { is = classLoadHelper.getResourceAsStream(QUARTZ_XSD_PATH_IN_JAR); if (is == null) { logger.warn("Could not load jobs schema from classpath!"); } else { inputSource = new InputSource(is); } return inputSource; } /** * 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 processFile(String fileName, boolean failOnFileNotFound) throws Exception { boolean fileFound = false; InputStream f = null; try { String furl = null; File file = new File(fileName); // files in filesystem if (!file.exists()) { URL url = classLoadHelper.getResource(fileName); if (url != null) { try { furl = URLDecoder.decode(url.getPath(), "UTF-8"); } catch (UnsupportedEncodingException e) { furl = url.getPath(); } file = new File(furl); try { f = url.openStream(); } catch (IOException ignor) { // Swallow the exception } } } else { try { f = new java.io.FileInputStream(file); } catch (FileNotFoundException e) { // ignore } } if (f == null) { fileFound = false; } else { fileFound = true; } } finally { try { if (f != null) { f.close(); } } catch (IOException ioe) { logger.warn("Error closing jobs file " + fileName, ioe); } } if (!fileFound) { if (failOnFileNotFound) { throw new SchedulerException("File named '" + fileName + "' does not exist."); } else { logger.warn("File named '" + fileName + "' does not exist. This is OK if you don't want to use an XML job config file."); } } else { processFile(fileName); } } /** * Process the xmlfile named fileName with the given system ID. * * @param fileName meta data file name. * @param systemId system ID. */ private void processFile(String fileName) throws ValidationException, ParserConfigurationException, SAXException, IOException, SchedulerException, ClassNotFoundException, ParseException, XPathException { prepForProcessing(); logger.info("Parsing XML file: " + fileName); InputSource is = new InputSource(getInputStream(fileName)); // is.setSystemId(systemId); process(is); maybeThrowValidationException(); } /* * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Interface. * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ private void prepForProcessing() { clearValidationExceptions(); loadedJobs.clear(); loadedTriggers.clear(); } private void process(InputSource is) throws SAXException, IOException, ParseException, XPathException, ClassNotFoundException { // load the document Document document = docBuilder.parse(is); // // Extract Job definitions... // NodeList jobNodes = (NodeList) xpath.evaluate("/job-scheduling-data/schedule/job", document, XPathConstants.NODESET); logger.debug("Found " + jobNodes.getLength() + " job definitions."); for (int i = 0; i < jobNodes.getLength(); i++) { Node jobDetailNode = jobNodes.item(i); String jobName = getTrimmedToNullString(xpath, "name", jobDetailNode); String jobDescription = getTrimmedToNullString(xpath, "description", jobDetailNode); String jobClassName = getTrimmedToNullString(xpath, "job-class", jobDetailNode); boolean isConcurrencyAllowed = getBoolean(xpath, "concurrency-allowed", jobDetailNode); Class jobClass = classLoadHelper.loadClass(jobClassName); JobDetail jobDetail = newJobBuilder(jobClass).withIdentity(jobName).isConcurrencyAllowed(isConcurrencyAllowed).withDescription(jobDescription) .build(); NodeList jobDataEntries = (NodeList) xpath.evaluate("job-data-map/entry", jobDetailNode, XPathConstants.NODESET); for (int k = 0; k < jobDataEntries.getLength(); k++) { Node entryNode = jobDataEntries.item(k); String key = getTrimmedToNullString(xpath, "key", entryNode); String value = getTrimmedToNullString(xpath, "value", entryNode); jobDetail.getJobDataMap().put(key, value); } logger.debug("Parsed job definition: " + jobDetail); loadedJobs.add(jobDetail); } // // Extract Trigger definitions... // NodeList triggerEntries = (NodeList) xpath.evaluate("/job-scheduling-data/schedule/trigger/*", document, XPathConstants.NODESET); logger.debug("Found " + triggerEntries.getLength() + " trigger definitions."); for (int j = 0; j < triggerEntries.getLength(); j++) { Node triggerNode = triggerEntries.item(j); String triggerName = getTrimmedToNullString(xpath, "name", triggerNode); String triggerDescription = getTrimmedToNullString(xpath, "description", triggerNode); String triggerMisfireInstructionConst = getTrimmedToNullString(xpath, "misfire-instruction", triggerNode); String triggerPriorityString = getTrimmedToNullString(xpath, "priority", triggerNode); String triggerCalendarRef = getTrimmedToNullString(xpath, "calendar-name", triggerNode); String triggerJobName = getTrimmedToNullString(xpath, "job-name", triggerNode); int triggerPriority = Trigger.DEFAULT_PRIORITY; if (triggerPriorityString != null) { triggerPriority = Integer.valueOf(triggerPriorityString); } String startTimeString = getTrimmedToNullString(xpath, "start-time", triggerNode); String startTimeFutureSecsString = getTrimmedToNullString(xpath, "start-time-seconds-in-future", triggerNode); String endTimeString = getTrimmedToNullString(xpath, "end-time", triggerNode); Date triggerStartTime = null; if (startTimeFutureSecsString != null) { triggerStartTime = new Date(System.currentTimeMillis() + (Long.valueOf(startTimeFutureSecsString) * 1000L)); } else { triggerStartTime = (startTimeString == null || startTimeString.length() == 0 ? new Date() : dateFormat.parse(startTimeString)); } Date triggerEndTime = endTimeString == null || endTimeString.length() == 0 ? null : dateFormat.parse(endTimeString); OperableTrigger trigger; if (triggerNode.getNodeName().equals("simple")) { String repeatCountString = getTrimmedToNullString(xpath, "repeat-count", triggerNode); String repeatIntervalString = getTrimmedToNullString(xpath, "repeat-interval", triggerNode); int repeatCount = repeatCountString == null ? SimpleTrigger.REPEAT_INDEFINITELY : Integer.parseInt(repeatCountString); long repeatInterval = repeatIntervalString == null ? 0 : Long.parseLong(repeatIntervalString); trigger = simpleTriggerBuilder().withRepeatCount(repeatCount).withIntervalInMilliseconds(repeatInterval).withIdentity(triggerName) .withDescription(triggerDescription).forJob(triggerJobName).startAt(triggerStartTime).endAt(triggerEndTime).withPriority(triggerPriority) .modifiedByCalendar(triggerCalendarRef).build(); if (triggerMisfireInstructionConst != null && triggerMisfireInstructionConst.length() != 0) { if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_FIRE_NOW")) { ((SimpleTriggerBuilder) trigger).withMisfireHandlingInstructionFireNow(); } else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT")) { ((SimpleTriggerBuilder) trigger).withMisfireHandlingInstructionNextWithExistingCount(); } else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT")) { ((SimpleTriggerBuilder) trigger).withMisfireHandlingInstructionNextWithRemainingCount(); } else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT")) { ((SimpleTriggerBuilder) trigger).withMisfireHandlingInstructionNowWithExistingCount(); } else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT")) { ((SimpleTriggerBuilder) trigger).withMisfireHandlingInstructionNowWithRemainingCount(); } else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_SMART_POLICY")) { // do nothing.... (smart policy is default) } else { throw new ParseException("Unexpected/Unhandlable Misfire Instruction encountered '" + triggerMisfireInstructionConst + "', for trigger: " + triggerName, -1); } } } else if (triggerNode.getNodeName().equals("cron")) { String cronExpression = getTrimmedToNullString(xpath, "cron-expression", triggerNode); String timezoneString = getTrimmedToNullString(xpath, "time-zone", triggerNode); TimeZone tz = timezoneString == null ? null : TimeZone.getTimeZone(timezoneString); trigger = cronTriggerBuilder(cronExpression).inTimeZone(tz).withIdentity(triggerName).withDescription(triggerDescription) .forJob(triggerJobName).startAt(triggerStartTime).endAt(triggerEndTime).withPriority(triggerPriority) .modifiedByCalendar(triggerCalendarRef).build(); if (triggerMisfireInstructionConst != null && triggerMisfireInstructionConst.length() != 0) { if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_DO_NOTHING")) { ((CronTriggerBuilder) trigger).withMisfireHandlingInstructionDoNothing(); } else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_FIRE_ONCE_NOW")) { ((CronTriggerBuilder) trigger).withMisfireHandlingInstructionFireAndProceed(); } else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_SMART_POLICY")) { // do nothing.... (smart policy is default) } else { throw new ParseException("Unexpected/Unhandlable Misfire Instruction encountered '" + triggerMisfireInstructionConst + "', for trigger: " + triggerName, -1); } } } else { throw new ParseException("Unknown trigger type: " + triggerNode.getNodeName(), -1); } NodeList jobDataEntries = (NodeList) xpath.evaluate("job-data-map/entry", triggerNode, XPathConstants.NODESET); for (int k = 0; k < jobDataEntries.getLength(); k++) { Node entryNode = jobDataEntries.item(k); String key = getTrimmedToNullString(xpath, "key", entryNode); String value = getTrimmedToNullString(xpath, "value", entryNode); trigger.getJobDataMap().put(key, value); } logger.debug("Parsed trigger definition: " + trigger); loadedTriggers.add(trigger); } } private String getTrimmedToNullString(XPath xpath, String elementName, Node parentNode) throws XPathExpressionException { String str = (String) xpath.evaluate(elementName, parentNode, XPathConstants.STRING); if (str != null) { str = str.trim(); } if (str != null && str.length() == 0) { str = null; } return str; } protected Boolean getBoolean(XPath xpathToElement, String elementName, Node parentNode) throws XPathExpressionException { String str = (String) xpath.evaluate(elementName, parentNode, XPathConstants.STRING); return str.equalsIgnoreCase("true"); } /** * Returns a List of jobs loaded from the xml file. *

* * @return a List of jobs. */ private List getLoadedJobs() { return Collections.unmodifiableList(loadedJobs); } /** * Returns a List of triggers loaded from the xml file. *

* * @return a List of triggers. */ private List getLoadedTriggers() { return Collections.unmodifiableList(loadedTriggers); } /** * Returns an InputStream from the fileName as a resource. * * @param fileName file name. * @return an InputStream from the fileName as a resource. */ private InputStream getInputStream(String fileName) { return this.classLoadHelper.getResourceAsStream(fileName); } /** * Schedules the given sets of jobs and triggers. * * @param sched job scheduler. * @exception SchedulerException if the Job or Trigger cannot be added to the Scheduler, or there is an internal Scheduler error. */ public void scheduleJobs(Scheduler sched) throws SchedulerException { List jobs = new LinkedList(getLoadedJobs()); List triggers = new LinkedList(getLoadedTriggers()); logger.info("Adding " + jobs.size() + " jobs, " + triggers.size() + " triggers."); for (JobDetail jobDetail : jobs) { logger.info("Scheduled job: {} ", jobDetail); sched.addJob(jobDetail); } for (OperableTrigger trigger : triggers) { logger.info("Scheduled trigger: {}", trigger); if (trigger.getStartTime() == null) { trigger.setStartTime(new Date()); } Trigger dupeT = sched.getTrigger(trigger.getName()); if (dupeT != null) { // if trigger with name already exists if (!dupeT.getJobName().equals(trigger.getJobName())) { logger.warn("Possibly duplicately named ({}) triggers in jobs xml file! ", trigger.getName()); } sched.rescheduleJob(trigger.getName(), trigger); } else { logger.debug("Scheduling job: " + trigger.getJobName() + " with trigger: " + trigger.getName()); try { sched.scheduleJob(trigger); } catch (ObjectAlreadyExistsException e) { logger.debug("Adding trigger: " + trigger.getName() + " for job: " + trigger.getJobName() + " failed because the trigger already existed."); } } } } /** * 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. */ @Override 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. */ @Override 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. */ @Override public void fatalError(SAXParseException e) throws SAXException { addValidationException(e); } /** * Adds a detected validation exception. * * @param e SAX exception. */ private void addValidationException(SAXException e) { validationExceptions.add(e); } /** * Resets the the number of detected validation exceptions. */ private void clearValidationExceptions() { validationExceptions.clear(); } /** * Throws a ValidationException if the number of validationExceptions detected is greater than zero. * * @exception ValidationException DTD validation exception. */ private void maybeThrowValidationException() throws ValidationException { if (validationExceptions.size() > 0) { throw new ValidationException("Encountered " + validationExceptions.size() + " validation exceptions.", validationExceptions); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy