001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.camel.component.quartz;
018
019 import java.io.IOException;
020 import java.io.InputStream;
021 import java.net.URI;
022 import java.text.ParseException;
023 import java.util.ArrayList;
024 import java.util.Date;
025 import java.util.List;
026 import java.util.Map;
027 import java.util.Properties;
028 import java.util.concurrent.atomic.AtomicInteger;
029
030 import org.apache.camel.CamelContext;
031 import org.apache.camel.StartupListener;
032 import org.apache.camel.impl.DefaultComponent;
033 import org.apache.camel.util.IOHelper;
034 import org.apache.camel.util.IntrospectionSupport;
035 import org.apache.camel.util.ObjectHelper;
036 import org.quartz.CronTrigger;
037 import org.quartz.JobDetail;
038 import org.quartz.Scheduler;
039 import org.quartz.SchedulerException;
040 import org.quartz.SchedulerFactory;
041 import org.quartz.SimpleTrigger;
042 import org.quartz.Trigger;
043 import org.quartz.impl.StdSchedulerFactory;
044 import org.slf4j.Logger;
045 import org.slf4j.LoggerFactory;
046
047 /**
048 * A <a href="http://camel.apache.org/quartz.html">Quartz Component</a>
049 * <p/>
050 * For a brief tutorial on setting cron expression see
051 * <a href="http://www.opensymphony.com/quartz/wikidocs/CronTriggers%20Tutorial.html">Quartz cron tutorial</a>.
052 *
053 * @version
054 */
055 public class QuartzComponent extends DefaultComponent implements StartupListener {
056 private static final transient Logger LOG = LoggerFactory.getLogger(QuartzComponent.class);
057 private final AtomicInteger jobs = new AtomicInteger();
058 private Scheduler scheduler;
059 private final List<JobToAdd> jobsToAdd = new ArrayList<JobToAdd>();
060 private SchedulerFactory factory;
061 private Properties properties;
062 private String propertiesFile;
063 private int startDelayedSeconds;
064 private boolean autoStartScheduler = true;
065
066 private final class JobToAdd {
067 private final JobDetail job;
068 private final Trigger trigger;
069
070 private JobToAdd(JobDetail job, Trigger trigger) {
071 this.job = job;
072 this.trigger = trigger;
073 }
074
075 public JobDetail getJob() {
076 return job;
077 }
078
079 public Trigger getTrigger() {
080 return trigger;
081 }
082 }
083
084 public QuartzComponent() {
085 }
086
087 public QuartzComponent(final CamelContext context) {
088 super(context);
089 }
090
091 @Override
092 protected QuartzEndpoint createEndpoint(final String uri, final String remaining, final Map<String, Object> parameters) throws Exception {
093
094 // lets split the remaining into a group/name
095 URI u = new URI(uri);
096 String path = ObjectHelper.after(u.getPath(), "/");
097 String host = u.getHost();
098 String cron = getAndRemoveParameter(parameters, "cron", String.class);
099 Boolean fireNow = getAndRemoveParameter(parameters, "fireNow", Boolean.class, Boolean.FALSE);
100
101 // group can be optional, if so set it to Camel
102 String name;
103 String group;
104 if (ObjectHelper.isNotEmpty(path) && ObjectHelper.isNotEmpty(host)) {
105 group = host;
106 name = path;
107 } else {
108 group = "Camel";
109 name = host;
110 }
111
112 Map<String, Object> triggerParameters = IntrospectionSupport.extractProperties(parameters, "trigger.");
113 Map<String, Object> jobParameters = IntrospectionSupport.extractProperties(parameters, "job.");
114
115 Trigger trigger;
116 boolean stateful = "true".equals(parameters.get("stateful"));
117
118 // if we're starting up and not running in Quartz clustered mode or not stateful then check for a name conflict.
119 if (!isClustered() && !stateful) {
120 // check to see if this trigger already exists
121 trigger = getScheduler().getTrigger(name, group);
122 if (trigger != null) {
123 String msg = "A Quartz job already exists with the name/group: " + name + "/" + group;
124 throw new IllegalArgumentException(msg);
125 }
126 }
127
128 // create the trigger either cron or simple
129 if (ObjectHelper.isNotEmpty(cron)) {
130 trigger = createCronTrigger(cron);
131 } else {
132 trigger = new SimpleTrigger();
133 if (fireNow) {
134 String intervalString = (String) triggerParameters.get("repeatInterval");
135 if (intervalString != null) {
136 long interval = Long.valueOf(intervalString);
137 trigger.setStartTime(new Date(System.currentTimeMillis() - interval));
138 }
139 }
140 }
141
142 QuartzEndpoint answer = new QuartzEndpoint(uri, this);
143 setProperties(answer.getJobDetail(), jobParameters);
144
145 setProperties(trigger, triggerParameters);
146 trigger.setName(name);
147 trigger.setGroup(group);
148 answer.setTrigger(trigger);
149
150 return answer;
151 }
152
153 protected CronTrigger createCronTrigger(String path) throws ParseException {
154 // replace + back to space so it's a cron expression
155 path = path.replaceAll("\\+", " ");
156 CronTrigger cron = new CronTrigger();
157 cron.setCronExpression(path);
158 return cron;
159 }
160
161 public void onCamelContextStarted(CamelContext camelContext, boolean alreadyStarted) throws Exception {
162 if (scheduler != null) {
163 // register current camel context to scheduler so we can look it up when jobs is being triggered
164 scheduler.getContext().put(QuartzConstants.QUARTZ_CAMEL_CONTEXT + "-" + getCamelContext().getName(), getCamelContext());
165 }
166
167 // if not configure to auto start then don't start it
168 if (!isAutoStartScheduler()) {
169 LOG.info("QuartzComponent configured to not auto start Quartz scheduler.");
170 return;
171 }
172
173 // only start scheduler when CamelContext have finished starting
174 startScheduler();
175 }
176
177 @Override
178 protected void doStart() throws Exception {
179 super.doStart();
180 if (scheduler == null) {
181 scheduler = getScheduler();
182 }
183 }
184
185 @Override
186 protected void doStop() throws Exception {
187 super.doStop();
188
189 if (scheduler != null) {
190 int number = jobs.get();
191 if (number > 0) {
192 LOG.info("Cannot shutdown Quartz scheduler: " + scheduler.getSchedulerName() + " as there are still " + number + " jobs registered.");
193 } else {
194 // no more jobs then shutdown the scheduler
195 LOG.info("There are no more jobs registered, so shutting down Quartz scheduler: " + scheduler.getSchedulerName());
196 scheduler.shutdown();
197 scheduler = null;
198 }
199 }
200 }
201
202 public void addJob(JobDetail job, Trigger trigger) throws SchedulerException {
203 if (scheduler == null) {
204 // add job to internal list because we will defer adding to the scheduler when camel context has been fully started
205 jobsToAdd.add(new JobToAdd(job, trigger));
206 } else {
207 // add job directly to scheduler
208 doAddJob(job, trigger);
209 }
210 }
211
212 private void doAddJob(JobDetail job, Trigger trigger) throws SchedulerException {
213 jobs.incrementAndGet();
214
215 Trigger existingTrigger = getScheduler().getTrigger(trigger.getName(), trigger.getGroup());
216 if (existingTrigger == null) {
217 if (LOG.isDebugEnabled()) {
218 LOG.debug("Adding job using trigger: " + trigger.getGroup() + "/" + trigger.getName());
219 }
220 getScheduler().scheduleJob(job, trigger);
221 } else if (hasTriggerChanged(existingTrigger, trigger)) {
222 if (LOG.isDebugEnabled()) {
223 LOG.debug("Trigger: " + trigger.getGroup() + "/" + trigger.getName() + " already exists and will be updated by Quartz.");
224 }
225 scheduler.addJob(job, true);
226 trigger.setJobName(job.getName());
227 scheduler.rescheduleJob(trigger.getName(), trigger.getGroup(), trigger);
228 } else {
229 if (LOG.isDebugEnabled()) {
230 LOG.debug("Trigger: " + trigger.getGroup() + "/" + trigger.getName() + " already exists and will be resumed automatically by Quartz.");
231 }
232 if (!isClustered()) {
233 scheduler.resumeTrigger(trigger.getName(), trigger.getGroup());
234 }
235 }
236 }
237
238 private boolean hasTriggerChanged(Trigger oldTrigger, Trigger newTrigger) {
239 if (oldTrigger instanceof CronTrigger && oldTrigger.equals(newTrigger)) {
240 CronTrigger oldCron = (CronTrigger) oldTrigger;
241 CronTrigger newCron = (CronTrigger) newTrigger;
242 return !oldCron.getCronExpression().equals(newCron.getCronExpression());
243 } else {
244 return !newTrigger.equals(oldTrigger);
245 }
246 }
247
248 public void pauseJob(Trigger trigger) throws SchedulerException {
249 jobs.decrementAndGet();
250
251 if (isClustered()) {
252 // do not pause jobs which are clustered, as we want the jobs to continue running on the other nodes
253 if (LOG.isDebugEnabled()) {
254 LOG.debug("Cannot pause job using trigger: " + trigger.getGroup() + "/" + trigger.getName() + " as the JobStore is clustered.");
255 }
256 } else {
257 if (LOG.isDebugEnabled()) {
258 LOG.debug("Pausing job using trigger: " + trigger.getGroup() + "/" + trigger.getName());
259 }
260 getScheduler().pauseTrigger(trigger.getName(), trigger.getGroup());
261 getScheduler().pauseJob(trigger.getName(), trigger.getGroup());
262 }
263 }
264
265 public void deleteJob(String name, String group) throws SchedulerException {
266 if (isClustered()) {
267 // do not pause jobs which are clustered, as we want the jobs to continue running on the other nodes
268 if (LOG.isDebugEnabled()) {
269 LOG.debug("Cannot delete job using trigger: " + group + "/" + name + " as the JobStore is clustered.");
270 }
271 } else {
272 Trigger trigger = getScheduler().getTrigger(name, group);
273 if (trigger != null) {
274 if (LOG.isDebugEnabled()) {
275 LOG.debug("Deleting job using trigger: " + group + "/" + name);
276 }
277 getScheduler().unscheduleJob(name, group);
278 }
279 }
280 }
281
282 /**
283 * To force shutdown the quartz scheduler
284 *
285 * @throws SchedulerException can be thrown if error shutting down
286 */
287 public void shutdownScheduler() throws SchedulerException {
288 if (scheduler != null) {
289 LOG.info("Forcing shutdown of Quartz scheduler: " + scheduler.getSchedulerName());
290 scheduler.shutdown();
291 scheduler = null;
292 }
293 }
294
295 /**
296 * Is the quartz scheduler clustered?
297 */
298 public boolean isClustered() throws SchedulerException {
299 try {
300 return getScheduler().getMetaData().isJobStoreClustered();
301 } catch (NoSuchMethodError e) {
302 LOG.debug("Job clustering is only supported since Quartz 1.7, isClustered returning false");
303 return false;
304 }
305 }
306
307 /**
308 * To force starting the quartz scheduler
309 *
310 * @throws SchedulerException can be thrown if error starting
311 */
312 public void startScheduler() throws SchedulerException {
313 for (JobToAdd add : jobsToAdd) {
314 doAddJob(add.getJob(), add.getTrigger());
315 }
316 jobsToAdd.clear();
317
318 if (!getScheduler().isStarted()) {
319 if (getStartDelayedSeconds() > 0) {
320 LOG.info("Starting Quartz scheduler: " + getScheduler().getSchedulerName() + " delayed: " + getStartDelayedSeconds() + " seconds.");
321 try {
322 getScheduler().startDelayed(getStartDelayedSeconds());
323 } catch (NoSuchMethodError e) {
324 LOG.warn("Your version of Quartz is too old to support delayed startup! "
325 + "Starting Quartz scheduler immediately : " + getScheduler().getSchedulerName());
326 getScheduler().start();
327 }
328 } else {
329 LOG.info("Starting Quartz scheduler: " + getScheduler().getSchedulerName());
330 getScheduler().start();
331 }
332 }
333 }
334
335 // Properties
336 // -------------------------------------------------------------------------
337
338 public SchedulerFactory getFactory() throws SchedulerException {
339 if (factory == null) {
340 factory = createSchedulerFactory();
341 }
342 return factory;
343 }
344
345 public void setFactory(SchedulerFactory factory) {
346 this.factory = factory;
347 }
348
349 public synchronized Scheduler getScheduler() throws SchedulerException {
350 if (scheduler == null) {
351 scheduler = createScheduler();
352 }
353 return scheduler;
354 }
355
356 public void setScheduler(final Scheduler scheduler) {
357 this.scheduler = scheduler;
358 }
359
360 public Properties getProperties() {
361 return properties;
362 }
363
364 public void setProperties(Properties properties) {
365 this.properties = properties;
366 }
367
368 public String getPropertiesFile() {
369 return propertiesFile;
370 }
371
372 public void setPropertiesFile(String propertiesFile) {
373 this.propertiesFile = propertiesFile;
374 }
375
376 public int getStartDelayedSeconds() {
377 return startDelayedSeconds;
378 }
379
380 public void setStartDelayedSeconds(int startDelayedSeconds) {
381 this.startDelayedSeconds = startDelayedSeconds;
382 }
383
384 public boolean isAutoStartScheduler() {
385 return autoStartScheduler;
386 }
387
388 public void setAutoStartScheduler(boolean autoStartScheduler) {
389 this.autoStartScheduler = autoStartScheduler;
390 }
391
392 // Implementation methods
393 // -------------------------------------------------------------------------
394
395 protected Properties loadProperties() throws SchedulerException {
396 Properties answer = getProperties();
397 if (answer == null && getPropertiesFile() != null) {
398 if (LOG.isInfoEnabled()) {
399 LOG.info("Loading Quartz properties file from classpath: " + getPropertiesFile());
400 }
401 InputStream is = getCamelContext().getClassResolver().loadResourceAsStream(getPropertiesFile());
402 if (is == null) {
403 throw new SchedulerException("Quartz properties file not found in classpath: " + getPropertiesFile());
404 }
405 answer = new Properties();
406 try {
407 answer.load(is);
408 } catch (IOException e) {
409 throw new SchedulerException("Error loading Quartz properties file from classpath: " + getPropertiesFile(), e);
410 }
411 }
412 return answer;
413 }
414
415 protected SchedulerFactory createSchedulerFactory() throws SchedulerException {
416 SchedulerFactory answer;
417
418 Properties prop = loadProperties();
419 if (prop != null) {
420
421 // force disabling update checker (will do online check over the internet)
422 prop.put("org.quartz.scheduler.skipUpdateCheck", "true");
423
424 answer = new StdSchedulerFactory(prop);
425 } else {
426 // read default props to be able to use a single scheduler per camel context
427 // if we need more than one scheduler per context use setScheduler(Scheduler)
428 // or setFactory(SchedulerFactory) methods
429
430 // must use classloader from StdSchedulerFactory to work even in OSGi
431 InputStream is = StdSchedulerFactory.class.getClassLoader().getResourceAsStream("org/quartz/quartz.properties");
432 if (is == null) {
433 throw new SchedulerException("Quartz properties file not found in classpath: org/quartz/quartz.properties");
434 }
435 prop = new Properties();
436 try {
437 prop.load(is);
438 } catch (IOException e) {
439 throw new SchedulerException("Error loading Quartz properties file from classpath: org/quartz/quartz.properties", e);
440 }
441
442 // camel context name will be a suffix to use one scheduler per context
443 String identity = getCamelContext().getName();
444
445 String instName = prop.getProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME);
446 if (instName == null) {
447 instName = "scheduler-" + identity;
448 } else {
449 instName = instName + "-" + identity;
450 }
451 prop.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, instName);
452
453 // force disabling update checker (will do online check over the internet)
454 prop.put("org.quartz.scheduler.skipUpdateCheck", "true");
455
456 answer = new StdSchedulerFactory(prop);
457 }
458
459 if (LOG.isDebugEnabled()) {
460 String name = prop.getProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME);
461 LOG.debug("Creating SchedulerFactory: " + name + " with properties: " + prop);
462 }
463 return answer;
464 }
465
466 protected Scheduler createScheduler() throws SchedulerException {
467 Scheduler scheduler = getFactory().getScheduler();
468 // register current camel context to scheduler so we can look it up when jobs is being triggered
469 scheduler.getContext().put(QuartzConstants.QUARTZ_CAMEL_CONTEXT + "-" + getCamelContext().getName(), getCamelContext());
470 return scheduler;
471 }
472
473 }