/** * 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.quartz2; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.Map; import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; import org.apache.camel.CamelContext; import org.apache.camel.Endpoint; import org.apache.camel.StartupListener; import org.apache.camel.impl.UriEndpointComponent; import org.apache.camel.spi.Metadata; 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.Scheduler; import org.quartz.SchedulerContext; import org.quartz.SchedulerException; import org.quartz.SchedulerFactory; import org.quartz.TriggerKey; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A factory for QuartzEndpoint. This component will hold a Quartz Scheduler that will provide scheduled timer based * endpoint that generate a QuartzMessage to a route. Currently it support Cron and Simple trigger scheduling type. * * <p>This component uses Quartz 2.x API and provide all the features from "camel-quartz". It has reused some * of the code, but mostly has been re-written in attempt to be more easier to maintain, and use Quartz more * fully.</p> */ public class QuartzComponent extends UriEndpointComponent implements StartupListener { private static final Logger LOG = LoggerFactory.getLogger(QuartzComponent.class); @Metadata(label = "advanced") private Scheduler scheduler; @Metadata(label = "advanced") private SchedulerFactory schedulerFactory; private Properties properties; private String propertiesFile; @Metadata(label = "scheduler") private int startDelayedSeconds; @Metadata(label = "scheduler", defaultValue = "true") private boolean autoStartScheduler = true; @Metadata(label = "scheduler") private boolean interruptJobsOnShutdown; @Metadata(defaultValue = "true") private boolean enableJmx = true; private boolean prefixJobNameWithEndpointId; @Metadata(defaultValue = "true") private boolean prefixInstanceName = true; public QuartzComponent() { super(QuartzEndpoint.class); } public QuartzComponent(CamelContext camelContext) { super(camelContext, QuartzEndpoint.class); } 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 int getStartDelayedSeconds() { return startDelayedSeconds; } /** * Seconds to wait before starting the quartz scheduler. */ public void setStartDelayedSeconds(int startDelayedSeconds) { this.startDelayedSeconds = startDelayedSeconds; } public boolean isPrefixJobNameWithEndpointId() { return prefixJobNameWithEndpointId; } /** * Whether to prefix the quartz job with the endpoint id. * <p/> * This option is default false. */ public void setPrefixJobNameWithEndpointId(boolean prefixJobNameWithEndpointId) { this.prefixJobNameWithEndpointId = prefixJobNameWithEndpointId; } 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; } 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 boolean isPrefixInstanceName() { return prefixInstanceName; } /** * Whether to prefix the Quartz Scheduler instance name with the CamelContext name. * <p/> * This is enabled by default, to let each CamelContext use its own Quartz scheduler instance by default. * You can set this option to <tt>false</tt> to reuse Quartz scheduler instances between multiple CamelContext's. */ public void setPrefixInstanceName(boolean prefixInstanceName) { this.prefixInstanceName = prefixInstanceName; } public boolean isInterruptJobsOnShutdown() { return interruptJobsOnShutdown; } /** * Whether to interrupt jobs on shutdown which forces the scheduler to shutdown quicker and attempt to interrupt any running jobs. * If this is enabled then any running jobs can fail due to being interrupted. */ public void setInterruptJobsOnShutdown(boolean interruptJobsOnShutdown) { this.interruptJobsOnShutdown = interruptJobsOnShutdown; } public SchedulerFactory getSchedulerFactory() throws SchedulerException { if (schedulerFactory == null) { schedulerFactory = createSchedulerFactory(); } return schedulerFactory; } private 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"); prop.put("org.terracotta.quartz.skipUpdateCheck", "true"); // camel context name will be a suffix to use one scheduler per context if (isPrefixInstanceName()) { String instName = createInstanceName(prop); prop.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, instName); } if (isInterruptJobsOnShutdown()) { prop.setProperty(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN, "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); } 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 if (isPrefixInstanceName()) { // 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"); prop.put("org.terracotta.quartz.skipUpdateCheck", "true"); if (isInterruptJobsOnShutdown()) { prop.setProperty(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN, "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; } /** * Is the quartz scheduler clustered? */ public boolean isClustered() throws SchedulerException { return getScheduler().getMetaData().isJobStoreClustered(); } private 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; } /** * To use the custom SchedulerFactory which is used to create the Scheduler. */ public void setSchedulerFactory(SchedulerFactory schedulerFactory) { this.schedulerFactory = schedulerFactory; } public Scheduler getScheduler() { return scheduler; } /** * To use the custom configured Quartz scheduler, instead of creating a new Scheduler. */ public void setScheduler(Scheduler scheduler) { this.scheduler = scheduler; } @Override protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception { // Get couple of scheduler settings Integer startDelayedSeconds = getAndRemoveParameter(parameters, "startDelayedSeconds", Integer.class); if (startDelayedSeconds != null) { 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; } } Boolean autoStartScheduler = getAndRemoveParameter(parameters, "autoStartScheduler", Boolean.class); if (autoStartScheduler != null) { this.autoStartScheduler = autoStartScheduler; } Boolean prefixJobNameWithEndpointId = getAndRemoveParameter(parameters, "prefixJobNameWithEndpointId", Boolean.class); if (prefixJobNameWithEndpointId != null) { this.prefixJobNameWithEndpointId = prefixJobNameWithEndpointId; } // Extract trigger.XXX and job.XXX properties to be set on endpoint below Map<String, Object> triggerParameters = IntrospectionSupport.extractProperties(parameters, "trigger."); Map<String, Object> jobParameters = IntrospectionSupport.extractProperties(parameters, "job."); // Create quartz endpoint QuartzEndpoint result = new QuartzEndpoint(uri, this); TriggerKey triggerKey = createTriggerKey(uri, remaining, result); result.setTriggerKey(triggerKey); result.setTriggerParameters(triggerParameters); result.setJobParameters(jobParameters); if (startDelayedSeconds != null) { result.setStartDelayedSeconds(startDelayedSeconds); } if (autoStartScheduler != null) { result.setAutoStartScheduler(autoStartScheduler); } if (prefixJobNameWithEndpointId != null) { result.setPrefixJobNameWithEndpointId(prefixJobNameWithEndpointId); } return result; } private TriggerKey createTriggerKey(String uri, String remaining, QuartzEndpoint endpoint) throws Exception { // Parse uri for trigger name and group URI u = new URI(uri); String path = ObjectHelper.after(u.getPath(), "/"); String host = u.getHost(); // 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; } } // Trigger group can be optional, if so set it to this context's unique name String name; String group; if (ObjectHelper.isNotEmpty(path) && ObjectHelper.isNotEmpty(host)) { group = host; name = path; } else { String camelContextName = QuartzHelper.getQuartzContextName(getCamelContext()); group = camelContextName == null ? "Camel" : "Camel_" + camelContextName; name = host; } if (prefixJobNameWithEndpointId) { name = endpoint.getId() + "_" + name; } return new TriggerKey(name, group); } @Override protected void doStart() throws Exception { super.doStart(); if (scheduler == null) { createAndInitScheduler(); } } private void createAndInitScheduler() throws SchedulerException { LOG.info("Create and initializing scheduler."); scheduler = createScheduler(); SchedulerContext quartzContext = storeCamelContextInQuartzContext(); // Set camel job counts to zero. We needed this to prevent shutdown in case there are multiple Camel contexts // that has not completed yet, and the last one with job counts to zero will eventually shutdown. AtomicInteger number = (AtomicInteger) quartzContext.get(QuartzConstants.QUARTZ_CAMEL_JOBS_COUNT); if (number == null) { number = new AtomicInteger(0); quartzContext.put(QuartzConstants.QUARTZ_CAMEL_JOBS_COUNT, number); } } private SchedulerContext storeCamelContextInQuartzContext() throws SchedulerException { // Store CamelContext into QuartzContext space SchedulerContext quartzContext = scheduler.getContext(); String camelContextName = QuartzHelper.getQuartzContextName(getCamelContext()); LOG.debug("Storing camelContextName={} into Quartz Context space.", camelContextName); quartzContext.put(QuartzConstants.QUARTZ_CAMEL_CONTEXT + "-" + camelContextName, getCamelContext()); return quartzContext; } private Scheduler createScheduler() throws SchedulerException { return getSchedulerFactory().getScheduler(); } @Override protected void doStop() throws Exception { super.doStop(); if (scheduler != null) { if (isInterruptJobsOnShutdown()) { LOG.info("Shutting down scheduler. (will interrupts jobs to shutdown quicker.)"); scheduler.shutdown(false); scheduler = null; } else { AtomicInteger number = (AtomicInteger) scheduler.getContext().get(QuartzConstants.QUARTZ_CAMEL_JOBS_COUNT); if (number != null && number.get() > 0) { LOG.info("Cannot shutdown scheduler: " + scheduler.getSchedulerName() + " as there are still " + number.get() + " jobs registered."); } else { LOG.info("Shutting down scheduler. (will wait for all jobs to complete first.)"); scheduler.shutdown(true); scheduler = null; } } } } @Override public void onCamelContextStarted(CamelContext context, boolean alreadyStarted) throws Exception { // If Camel has already started and then user add a route dynamically, we need to ensure // to create and init the scheduler first. if (scheduler == null) { createAndInitScheduler(); } else { // in case custom scheduler was injected (i.e. created elsewhere), we may need to add // current camel context to quartz context so jobs have access storeCamelContextInQuartzContext(); } // Now scheduler is ready, let see how we should start it. if (!autoStartScheduler) { LOG.info("Not starting scheduler because autoStartScheduler is set to false."); } else { if (startDelayedSeconds > 0) { if (scheduler.isStarted()) { LOG.warn("The scheduler has already started. Cannot apply the 'startDelayedSeconds' configuration!"); } else { LOG.info("Starting scheduler with startDelayedSeconds={}", startDelayedSeconds); scheduler.startDelayed(startDelayedSeconds); } } else { if (scheduler.isStarted()) { LOG.info("The scheduler has already been started."); } else { LOG.info("Starting scheduler."); scheduler.start(); } } } } }