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 LOG.debug("Adding job using trigger: {}/{}", trigger.getGroup(), trigger.getName());
218 getScheduler().scheduleJob(job, trigger);
219 } else if (hasTriggerChanged(existingTrigger, trigger)) {
220 LOG.debug("Trigger: {}/{} already exists and will be updated by Quartz.", trigger.getGroup(), trigger.getName());
221 scheduler.addJob(job, true);
222 trigger.setJobName(job.getName());
223 scheduler.rescheduleJob(trigger.getName(), trigger.getGroup(), trigger);
224 } else {
225 LOG.debug("Trigger: {}/{} already exists and will be resumed automatically by Quartz.", trigger.getGroup(), trigger.getName());
226 if (!isClustered()) {
227 scheduler.resumeTrigger(trigger.getName(), trigger.getGroup());
228 }
229 }
230 }
231
232 private boolean hasTriggerChanged(Trigger oldTrigger, Trigger newTrigger) {
233 if (oldTrigger instanceof CronTrigger && oldTrigger.equals(newTrigger)) {
234 CronTrigger oldCron = (CronTrigger) oldTrigger;
235 CronTrigger newCron = (CronTrigger) newTrigger;
236 return !oldCron.getCronExpression().equals(newCron.getCronExpression());
237 } else {
238 return !newTrigger.equals(oldTrigger);
239 }
240 }
241
242 public void pauseJob(Trigger trigger) throws SchedulerException {
243 jobs.decrementAndGet();
244
245 if (isClustered()) {
246 // do not pause jobs which are clustered, as we want the jobs to continue running on the other nodes
247 LOG.debug("Cannot pause job using trigger: {}/{} as the JobStore is clustered.", trigger.getGroup(), trigger.getName());
248 } else {
249 LOG.debug("Pausing job using trigger: {}/{}", trigger.getGroup(), trigger.getName());
250 getScheduler().pauseTrigger(trigger.getName(), trigger.getGroup());
251 getScheduler().pauseJob(trigger.getName(), trigger.getGroup());
252 }
253 }
254
255 public void deleteJob(String name, String group) throws SchedulerException {
256 if (isClustered()) {
257 // do not pause jobs which are clustered, as we want the jobs to continue running on the other nodes
258 LOG.debug("Cannot delete job using trigger: {}/{} as the JobStore is clustered.", group, name);
259 } else {
260 Trigger trigger = getScheduler().getTrigger(name, group);
261 if (trigger != null) {
262 LOG.debug("Deleting job using trigger: {}/{}", group, name);
263 getScheduler().unscheduleJob(name, group);
264 }
265 }
266 }
267
268 /**
269 * To force shutdown the quartz scheduler
270 *
271 * @throws SchedulerException can be thrown if error shutting down
272 */
273 public void shutdownScheduler() throws SchedulerException {
274 if (scheduler != null) {
275 LOG.info("Forcing shutdown of Quartz scheduler: " + scheduler.getSchedulerName());
276 scheduler.shutdown();
277 scheduler = null;
278 }
279 }
280
281 /**
282 * Is the quartz scheduler clustered?
283 */
284 public boolean isClustered() throws SchedulerException {
285 try {
286 return getScheduler().getMetaData().isJobStoreClustered();
287 } catch (NoSuchMethodError e) {
288 LOG.debug("Job clustering is only supported since Quartz 1.7, isClustered returning false");
289 return false;
290 }
291 }
292
293 /**
294 * To force starting the quartz scheduler
295 *
296 * @throws SchedulerException can be thrown if error starting
297 */
298 public void startScheduler() throws SchedulerException {
299 for (JobToAdd add : jobsToAdd) {
300 doAddJob(add.getJob(), add.getTrigger());
301 }
302 jobsToAdd.clear();
303
304 if (!getScheduler().isStarted()) {
305 if (getStartDelayedSeconds() > 0) {
306 LOG.info("Starting Quartz scheduler: " + getScheduler().getSchedulerName() + " delayed: " + getStartDelayedSeconds() + " seconds.");
307 try {
308 getScheduler().startDelayed(getStartDelayedSeconds());
309 } catch (NoSuchMethodError e) {
310 LOG.warn("Your version of Quartz is too old to support delayed startup! "
311 + "Starting Quartz scheduler immediately : " + getScheduler().getSchedulerName());
312 getScheduler().start();
313 }
314 } else {
315 LOG.info("Starting Quartz scheduler: " + getScheduler().getSchedulerName());
316 getScheduler().start();
317 }
318 }
319 }
320
321 // Properties
322 // -------------------------------------------------------------------------
323
324 public SchedulerFactory getFactory() throws SchedulerException {
325 if (factory == null) {
326 factory = createSchedulerFactory();
327 }
328 return factory;
329 }
330
331 public void setFactory(SchedulerFactory factory) {
332 this.factory = factory;
333 }
334
335 public synchronized Scheduler getScheduler() throws SchedulerException {
336 if (scheduler == null) {
337 scheduler = createScheduler();
338 }
339 return scheduler;
340 }
341
342 public void setScheduler(final Scheduler scheduler) {
343 this.scheduler = scheduler;
344 }
345
346 public Properties getProperties() {
347 return properties;
348 }
349
350 public void setProperties(Properties properties) {
351 this.properties = properties;
352 }
353
354 public String getPropertiesFile() {
355 return propertiesFile;
356 }
357
358 public void setPropertiesFile(String propertiesFile) {
359 this.propertiesFile = propertiesFile;
360 }
361
362 public int getStartDelayedSeconds() {
363 return startDelayedSeconds;
364 }
365
366 public void setStartDelayedSeconds(int startDelayedSeconds) {
367 this.startDelayedSeconds = startDelayedSeconds;
368 }
369
370 public boolean isAutoStartScheduler() {
371 return autoStartScheduler;
372 }
373
374 public void setAutoStartScheduler(boolean autoStartScheduler) {
375 this.autoStartScheduler = autoStartScheduler;
376 }
377
378 // Implementation methods
379 // -------------------------------------------------------------------------
380
381 protected Properties loadProperties() throws SchedulerException {
382 Properties answer = getProperties();
383 if (answer == null && getPropertiesFile() != null) {
384 LOG.info("Loading Quartz properties file from classpath: {}", getPropertiesFile());
385 InputStream is = getCamelContext().getClassResolver().loadResourceAsStream(getPropertiesFile());
386 if (is == null) {
387 throw new SchedulerException("Quartz properties file not found in classpath: " + getPropertiesFile());
388 }
389 answer = new Properties();
390 try {
391 answer.load(is);
392 } catch (IOException e) {
393 throw new SchedulerException("Error loading Quartz properties file from classpath: " + getPropertiesFile(), e);
394 }
395 }
396 return answer;
397 }
398
399 protected SchedulerFactory createSchedulerFactory() throws SchedulerException {
400 SchedulerFactory answer;
401
402 Properties prop = loadProperties();
403 if (prop != null) {
404
405 // force disabling update checker (will do online check over the internet)
406 prop.put("org.quartz.scheduler.skipUpdateCheck", "true");
407
408 answer = new StdSchedulerFactory(prop);
409 } else {
410 // read default props to be able to use a single scheduler per camel context
411 // if we need more than one scheduler per context use setScheduler(Scheduler)
412 // or setFactory(SchedulerFactory) methods
413
414 // must use classloader from StdSchedulerFactory to work even in OSGi
415 InputStream is = StdSchedulerFactory.class.getClassLoader().getResourceAsStream("org/quartz/quartz.properties");
416 if (is == null) {
417 throw new SchedulerException("Quartz properties file not found in classpath: org/quartz/quartz.properties");
418 }
419 prop = new Properties();
420 try {
421 prop.load(is);
422 } catch (IOException e) {
423 throw new SchedulerException("Error loading Quartz properties file from classpath: org/quartz/quartz.properties", e);
424 }
425
426 // camel context name will be a suffix to use one scheduler per context
427 String identity = getCamelContext().getName();
428
429 String instName = prop.getProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME);
430 if (instName == null) {
431 instName = "scheduler-" + identity;
432 } else {
433 instName = instName + "-" + identity;
434 }
435 prop.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, instName);
436
437 // force disabling update checker (will do online check over the internet)
438 prop.put("org.quartz.scheduler.skipUpdateCheck", "true");
439
440 answer = new StdSchedulerFactory(prop);
441 }
442
443 if (LOG.isDebugEnabled()) {
444 String name = prop.getProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME);
445 LOG.debug("Creating SchedulerFactory: {} with properties: {}", name, prop);
446 }
447 return answer;
448 }
449
450 protected Scheduler createScheduler() throws SchedulerException {
451 Scheduler scheduler = getFactory().getScheduler();
452 // register current camel context to scheduler so we can look it up when jobs is being triggered
453 scheduler.getContext().put(QuartzConstants.QUARTZ_CAMEL_CONTEXT + "-" + getCamelContext().getName(), getCamelContext());
454 return scheduler;
455 }
456
457 }