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