/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.camel.component.quartz; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.text.ParseException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import org.apache.camel.CamelContext; import org.apache.camel.StartupListener; import org.apache.camel.impl.UriEndpointComponent; import org.apache.camel.spi.Metadata; import org.apache.camel.util.EndpointHelper; import org.apache.camel.util.IOHelper; import org.apache.camel.util.IntrospectionSupport; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.ResourceHelper; import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SchedulerFactory; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A <a href="http://camel.apache.org/quartz.html">Quartz Component</a> * <p/> * For a brief tutorial on setting cron expression see * <a href="http://quartz-scheduler.org/documentation/quartz-1.x/tutorials/crontrigger">Quartz cron tutorial</a>. * * @version */ public class QuartzComponent extends UriEndpointComponent implements StartupListener { private static final Logger LOG = LoggerFactory.getLogger(QuartzComponent.class); private final transient List<JobToAdd> jobsToAdd = new ArrayList<JobToAdd>(); @Metadata(label = "advanced") private Scheduler scheduler; @Metadata(label = "advanced") private SchedulerFactory factory; private Properties properties; private String propertiesFile; @Metadata(label = "scheduler") private int startDelayedSeconds; @Metadata(defaultValue = "true") private boolean autoStartScheduler = true; @Metadata(defaultValue = "true") private boolean enableJmx = true; private static final class JobToAdd { private final JobDetail job; private final Trigger trigger; private JobToAdd(JobDetail job, Trigger trigger) { this.job = job; this.trigger = trigger; } public JobDetail getJob() { return job; } public Trigger getTrigger() { return trigger; } } public QuartzComponent() { super(QuartzEndpoint.class); } public QuartzComponent(final CamelContext context) { super(context, QuartzEndpoint.class); } @Override protected QuartzEndpoint createEndpoint(final String uri, final String remaining, final Map<String, Object> parameters) throws Exception { // lets split the remaining into a group/name URI u = new URI(uri); String path = ObjectHelper.after(u.getPath(), "/"); String host = u.getHost(); String cron = getAndRemoveParameter(parameters, "cron", String.class); boolean fireNow = getAndRemoveParameter(parameters, "fireNow", Boolean.class, Boolean.FALSE); Integer startDelayedSeconds = getAndRemoveParameter(parameters, "startDelayedSeconds", Integer.class); if (startDelayedSeconds != null) { if (scheduler.isStarted()) { LOG.warn("A Quartz job is already started. Cannot apply the 'startDelayedSeconds' configuration!"); } else if (this.startDelayedSeconds != 0 && !(this.startDelayedSeconds == startDelayedSeconds)) { LOG.warn("A Quartz job is already configured with a different 'startDelayedSeconds' configuration! " + "All Quartz jobs must share the same 'startDelayedSeconds' configuration! Cannot apply the 'startDelayedSeconds' configuration!"); } else { this.startDelayedSeconds = startDelayedSeconds; } } // host can be null if the uri did contain invalid host characters such as an underscore if (host == null) { host = ObjectHelper.before(remaining, "/"); if (host == null) { host = remaining; } } // group can be optional, if so set it to Camel String name; String group; if (ObjectHelper.isNotEmpty(path) && ObjectHelper.isNotEmpty(host)) { group = host; name = path; } else { group = "Camel"; name = host; } Map<String, Object> triggerParameters = IntrospectionSupport.extractProperties(parameters, "trigger."); Map<String, Object> jobParameters = IntrospectionSupport.extractProperties(parameters, "job."); Trigger trigger; boolean stateful = "true".equals(parameters.get("stateful")); // if we're starting up and not running in Quartz clustered mode or not stateful then check for a name conflict. if (!isClustered() && !stateful) { // check to see if this trigger already exists trigger = getScheduler().getTrigger(name, group); if (trigger != null) { String msg = "A Quartz job already exists with the name/group: " + name + "/" + group; throw new IllegalArgumentException(msg); } } // create the trigger either cron or simple if (ObjectHelper.isNotEmpty(cron)) { cron = encodeCronExpression(cron); trigger = createCronTrigger(cron); } else { trigger = new SimpleTrigger(); if (fireNow) { String intervalString = (String) triggerParameters.get("repeatInterval"); if (intervalString != null) { long interval = EndpointHelper.resolveParameter(getCamelContext(), intervalString, Long.class); trigger.setStartTime(new Date(System.currentTimeMillis() - interval)); } } } QuartzEndpoint answer = new QuartzEndpoint(uri, this); answer.setGroupName(group); answer.setTimerName(name); answer.setCron(cron); answer.setFireNow(fireNow); if (startDelayedSeconds != null) { answer.setStartDelayedSeconds(startDelayedSeconds); } if (triggerParameters != null && !triggerParameters.isEmpty()) { answer.setTriggerParameters(triggerParameters); } if (jobParameters != null && !jobParameters.isEmpty()) { answer.setJobParameters(jobParameters); setProperties(answer.getJobDetail(), jobParameters); } // enrich job data map with trigger information if (cron != null) { answer.getJobDetail().getJobDataMap().put(QuartzConstants.QUARTZ_TRIGGER_TYPE, "cron"); answer.getJobDetail().getJobDataMap().put(QuartzConstants.QUARTZ_TRIGGER_CRON_EXPRESSION, cron); String timeZone = EndpointHelper.resolveParameter(getCamelContext(), (String)triggerParameters.get("timeZone"), String.class); if (timeZone != null) { answer.getJobDetail().getJobDataMap().put(QuartzConstants.QUARTZ_TRIGGER_CRON_TIMEZONE, timeZone); } } else { answer.getJobDetail().getJobDataMap().put(QuartzConstants.QUARTZ_TRIGGER_TYPE, "simple"); Long interval = EndpointHelper.resolveParameter(getCamelContext(), (String)triggerParameters.get("repeatInterval"), Long.class); if (interval != null) { triggerParameters.put("repeatInterval", interval); answer.getJobDetail().getJobDataMap().put(QuartzConstants.QUARTZ_TRIGGER_SIMPLE_REPEAT_INTERVAL, interval); } Integer counter = EndpointHelper.resolveParameter(getCamelContext(), (String)triggerParameters.get("repeatCount"), Integer.class); if (counter != null) { triggerParameters.put("repeatCount", counter); answer.getJobDetail().getJobDataMap().put(QuartzConstants.QUARTZ_TRIGGER_SIMPLE_REPEAT_COUNTER, counter); } } setProperties(trigger, triggerParameters); trigger.setName(name); trigger.setGroup(group); answer.setTrigger(trigger); return answer; } protected CronTrigger createCronTrigger(String path) throws ParseException { CronTrigger cron = new CronTrigger(); cron.setCronExpression(path); return cron; } private static String encodeCronExpression(String path) { // replace + back to space so it's a cron expression return path.replaceAll("\\+", " "); } public void onCamelContextStarted(CamelContext camelContext, boolean alreadyStarted) throws Exception { if (scheduler != null) { String uid = QuartzHelper.getQuartzContextName(camelContext); scheduler.getContext().put(QuartzConstants.QUARTZ_CAMEL_CONTEXT + "-" + uid, camelContext); } // if not configure to auto start then don't start it if (!isAutoStartScheduler()) { LOG.info("QuartzComponent configured to not auto start Quartz scheduler."); return; } // only start scheduler when CamelContext has finished starting startScheduler(); } @Override protected void doStart() throws Exception { super.doStart(); if (scheduler == null) { scheduler = getScheduler(); } } @Override protected void doStop() throws Exception { super.doStop(); if (scheduler != null) { AtomicInteger number = (AtomicInteger) scheduler.getContext().get("CamelJobs"); if (number != null && number.get() > 0) { LOG.info("Cannot shutdown Quartz scheduler: " + scheduler.getSchedulerName() + " as there are still " + number.get() + " jobs registered."); } else { // no more jobs then shutdown the scheduler LOG.info("There are no more jobs registered, so shutting down Quartz scheduler: " + scheduler.getSchedulerName()); scheduler.shutdown(); scheduler = null; } } } public void addJob(JobDetail job, Trigger trigger) throws SchedulerException { if (scheduler == null) { // add job to internal list because we will defer adding to the scheduler when camel context has been fully started jobsToAdd.add(new JobToAdd(job, trigger)); } else { // add job directly to scheduler doAddJob(job, trigger); } } private void doAddJob(JobDetail job, Trigger trigger) throws SchedulerException { Trigger existingTrigger = getScheduler().getTrigger(trigger.getName(), trigger.getGroup()); if (existingTrigger == null) { LOG.debug("Adding job using trigger: {}/{}", trigger.getGroup(), trigger.getName()); getScheduler().scheduleJob(job, trigger); } else if (hasTriggerChanged(existingTrigger, trigger)) { LOG.debug("Trigger: {}/{} already exists and will be updated by Quartz.", trigger.getGroup(), trigger.getName()); // fast forward start time to now, as we do not want any misfire to kick in trigger.setStartTime(new Date()); // To ensure trigger uses the same job (the job name might change!) we will remove old trigger then re-add. scheduler.unscheduleJob(trigger.getName(), trigger.getGroup()); scheduler.addJob(job, true); trigger.setJobName(job.getName()); trigger.setJobGroup(job.getGroup()); scheduler.scheduleJob(trigger); } else { if (!isClustered()) { LOG.debug("Trigger: {}/{} already exists and will be resumed by Quartz.", trigger.getGroup(), trigger.getName()); // fast forward start time to now, as we do not want any misfire to kick in trigger.setStartTime(new Date()); // To ensure trigger uses the same job (the job name might change!) we will remove old trigger then re-add. scheduler.unscheduleJob(trigger.getName(), trigger.getGroup()); scheduler.addJob(job, true); trigger.setJobName(job.getName()); trigger.setJobGroup(job.getGroup()); scheduler.scheduleJob(trigger); } else { LOG.debug("Trigger: {}/{} already exists and is already scheduled by clustered JobStore.", trigger.getGroup(), trigger.getName()); } } // only increment job counter if we are successful incrementJobCounter(getScheduler()); } private static boolean hasTriggerChanged(Trigger oldTrigger, Trigger newTrigger) { if (newTrigger instanceof CronTrigger && oldTrigger instanceof CronTrigger) { CronTrigger newCron = (CronTrigger) newTrigger; CronTrigger oldCron = (CronTrigger) oldTrigger; return !newCron.getCronExpression().equals(oldCron.getCronExpression()); } else if (newTrigger instanceof SimpleTrigger && oldTrigger instanceof SimpleTrigger) { SimpleTrigger newSimple = (SimpleTrigger) newTrigger; SimpleTrigger oldSimple = (SimpleTrigger) oldTrigger; return newSimple.getRepeatInterval() != oldSimple.getRepeatInterval() || newSimple.getRepeatCount() != oldSimple.getRepeatCount(); } else { return !newTrigger.getClass().equals(oldTrigger.getClass()) || !newTrigger.equals(oldTrigger); } } public void pauseJob(Trigger trigger) throws SchedulerException { if (isClustered()) { // do not pause jobs which are clustered, as we want the jobs to continue running on the other nodes LOG.debug("Cannot pause job using trigger: {}/{} as the JobStore is clustered.", trigger.getGroup(), trigger.getName()); } else { LOG.debug("Pausing job using trigger: {}/{}", trigger.getGroup(), trigger.getName()); getScheduler().pauseTrigger(trigger.getName(), trigger.getGroup()); getScheduler().pauseJob(trigger.getName(), trigger.getGroup()); } // only decrement job counter if we are successful decrementJobCounter(getScheduler()); } public void deleteJob(String name, String group) throws SchedulerException { if (isClustered()) { // do not pause jobs which are clustered, as we want the jobs to continue running on the other nodes LOG.debug("Cannot delete job using trigger: {}/{} as the JobStore is clustered.", group, name); } else { Trigger trigger = getScheduler().getTrigger(name, group); if (trigger != null) { LOG.debug("Deleting job using trigger: {}/{}", group, name); getScheduler().unscheduleJob(name, group); } } } /** * To force shutdown the quartz scheduler * * @throws SchedulerException can be thrown if error shutting down */ public void shutdownScheduler() throws SchedulerException { if (scheduler != null) { LOG.info("Forcing shutdown of Quartz scheduler: " + scheduler.getSchedulerName()); scheduler.shutdown(); scheduler = null; } } /** * Is the quartz scheduler clustered? */ public boolean isClustered() throws SchedulerException { try { return getScheduler().getMetaData().isJobStoreClustered(); } catch (NoSuchMethodError e) { LOG.debug("Job clustering is only supported since Quartz 1.7, isClustered returning false"); return false; } } /** * To force starting the quartz scheduler * * @throws SchedulerException can be thrown if error starting */ public void startScheduler() throws SchedulerException { for (JobToAdd add : jobsToAdd) { doAddJob(add.getJob(), add.getTrigger()); } jobsToAdd.clear(); if (!getScheduler().isStarted()) { if (getStartDelayedSeconds() > 0) { LOG.info("Starting Quartz scheduler: " + getScheduler().getSchedulerName() + " delayed: " + getStartDelayedSeconds() + " seconds."); try { getScheduler().startDelayed(getStartDelayedSeconds()); } catch (NoSuchMethodError e) { LOG.warn("Your version of Quartz is too old to support delayed startup! " + "Starting Quartz scheduler immediately : " + getScheduler().getSchedulerName()); getScheduler().start(); } } else { LOG.info("Starting Quartz scheduler: " + getScheduler().getSchedulerName()); getScheduler().start(); } } } // Properties // ------------------------------------------------------------------------- public SchedulerFactory getFactory() throws SchedulerException { if (factory == null) { factory = createSchedulerFactory(); } return factory; } /** * To use the custom SchedulerFactory which is used to create the Scheduler. */ public void setFactory(SchedulerFactory factory) { this.factory = factory; } public synchronized Scheduler getScheduler() throws SchedulerException { if (scheduler == null) { scheduler = createScheduler(); } return scheduler; } /** * To use the custom configured Quartz scheduler, instead of creating a new Scheduler. */ public void setScheduler(final Scheduler scheduler) { this.scheduler = scheduler; } public Properties getProperties() { return properties; } /** * Properties to configure the Quartz scheduler. */ public void setProperties(Properties properties) { this.properties = properties; } public String getPropertiesFile() { return propertiesFile; } /** * File name of the properties to load from the classpath */ public void setPropertiesFile(String propertiesFile) { this.propertiesFile = propertiesFile; } public int getStartDelayedSeconds() { return startDelayedSeconds; } /** * Seconds to wait before starting the quartz scheduler. */ public void setStartDelayedSeconds(int startDelayedSeconds) { this.startDelayedSeconds = startDelayedSeconds; } public boolean isAutoStartScheduler() { return autoStartScheduler; } /** * Whether or not the scheduler should be auto started. * <p/> * This options is default true */ public void setAutoStartScheduler(boolean autoStartScheduler) { this.autoStartScheduler = autoStartScheduler; } public boolean isEnableJmx() { return enableJmx; } /** * Whether to enable Quartz JMX which allows to manage the Quartz scheduler from JMX. * <p/> * This options is default true */ public void setEnableJmx(boolean enableJmx) { this.enableJmx = enableJmx; } // Implementation methods // ------------------------------------------------------------------------- protected Properties loadProperties() throws SchedulerException { Properties answer = getProperties(); if (answer == null && getPropertiesFile() != null) { LOG.info("Loading Quartz properties file from: {}", getPropertiesFile()); InputStream is = null; try { is = ResourceHelper.resolveMandatoryResourceAsInputStream(getCamelContext(), getPropertiesFile()); answer = new Properties(); answer.load(is); } catch (IOException e) { throw new SchedulerException("Error loading Quartz properties file: " + getPropertiesFile(), e); } finally { IOHelper.close(is); } } return answer; } protected SchedulerFactory createSchedulerFactory() throws SchedulerException { SchedulerFactory answer; Properties prop = loadProperties(); if (prop != null) { // force disabling update checker (will do online check over the internet) prop.put("org.quartz.scheduler.skipUpdateCheck", "true"); // camel context name will be a suffix to use one scheduler per context String instName = createInstanceName(prop); prop.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, instName); // enable jmx unless configured to not do so if (enableJmx && !prop.containsKey("org.quartz.scheduler.jmx.export")) { LOG.info("Setting org.quartz.scheduler.jmx.export=true to ensure QuartzScheduler(s) will be enlisted in JMX."); prop.put("org.quartz.scheduler.jmx.export", "true"); } answer = new StdSchedulerFactory(prop); } else { // read default props to be able to use a single scheduler per camel context // if we need more than one scheduler per context use setScheduler(Scheduler) // or setFactory(SchedulerFactory) methods // must use classloader from StdSchedulerFactory to work even in OSGi InputStream is = StdSchedulerFactory.class.getClassLoader().getResourceAsStream("org/quartz/quartz.properties"); if (is == null) { throw new SchedulerException("Quartz properties file not found in classpath: org/quartz/quartz.properties"); } prop = new Properties(); try { prop.load(is); } catch (IOException e) { throw new SchedulerException("Error loading Quartz properties file from classpath: org/quartz/quartz.properties", e); } finally { IOHelper.close(is); } // camel context name will be a suffix to use one scheduler per context String instName = createInstanceName(prop); prop.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, instName); // force disabling update checker (will do online check over the internet) prop.put("org.quartz.scheduler.skipUpdateCheck", "true"); // enable jmx unless configured to not do so if (enableJmx && !prop.containsKey("org.quartz.scheduler.jmx.export")) { prop.put("org.quartz.scheduler.jmx.export", "true"); LOG.info("Setting org.quartz.scheduler.jmx.export=true to ensure QuartzScheduler(s) will be enlisted in JMX."); } answer = new StdSchedulerFactory(prop); } if (LOG.isDebugEnabled()) { String name = prop.getProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME); LOG.debug("Creating SchedulerFactory: {} with properties: {}", name, prop); } return answer; } protected String createInstanceName(Properties prop) { String instName = prop.getProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME); // camel context name will be a suffix to use one scheduler per context String identity = QuartzHelper.getQuartzContextName(getCamelContext()); if (identity != null) { if (instName == null) { instName = "scheduler-" + identity; } else { instName = instName + "-" + identity; } } return instName; } protected Scheduler createScheduler() throws SchedulerException { Scheduler scheduler = getFactory().getScheduler(); if (LOG.isDebugEnabled()) { LOG.debug("Using SchedulerFactory {} to get/create Scheduler {}({})", new Object[]{getFactory(), scheduler, ObjectHelper.getIdentityHashCode(scheduler)}); } // register current camel context to scheduler so we can look it up when jobs is being triggered // must use management name as it should be unique in the same JVM String uid = QuartzHelper.getQuartzContextName(getCamelContext()); scheduler.getContext().put(QuartzConstants.QUARTZ_CAMEL_CONTEXT + "-" + uid, getCamelContext()); // store Camel job counter AtomicInteger number = (AtomicInteger) scheduler.getContext().get("CamelJobs"); if (number == null) { number = new AtomicInteger(0); scheduler.getContext().put("CamelJobs", number); } return scheduler; } private static void decrementJobCounter(Scheduler scheduler) throws SchedulerException { AtomicInteger number = (AtomicInteger) scheduler.getContext().get("CamelJobs"); if (number != null) { number.decrementAndGet(); } } private static void incrementJobCounter(Scheduler scheduler) throws SchedulerException { AtomicInteger number = (AtomicInteger) scheduler.getContext().get("CamelJobs"); if (number != null) { number.incrementAndGet(); } } }