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 // create the trigger either cron or simple
115 Trigger trigger;
116 if (ObjectHelper.isNotEmpty(cron)) {
117 trigger = createCronTrigger(cron);
118 } else {
119 trigger = new SimpleTrigger();
120 if (fireNow) {
121 String intervalString = (String) triggerParameters.get("repeatInterval");
122 if (intervalString != null) {
123 long interval = Long.valueOf(intervalString);
124 trigger.setStartTime(new Date(System.currentTimeMillis() - interval));
125 }
126 }
127 }
128
129 QuartzEndpoint answer = new QuartzEndpoint(uri, this);
130 setProperties(answer.getJobDetail(), jobParameters);
131
132 setProperties(trigger, triggerParameters);
133 trigger.setName(name);
134 trigger.setGroup(group);
135 answer.setTrigger(trigger);
136
137 return answer;
138 }
139
140 protected CronTrigger createCronTrigger(String path) throws ParseException {
141 // replace + back to space so it's a cron expression
142 path = path.replaceAll("\\+", " ");
143 CronTrigger cron = new CronTrigger();
144 cron.setCronExpression(path);
145 return cron;
146 }
147
148 public void onCamelContextStarted(CamelContext camelContext, boolean alreadyStarted) throws Exception {
149 if (scheduler != null) {
150 // register current camel context to scheduler so we can look it up when jobs is being triggered
151 scheduler.getContext().put(QuartzConstants.QUARTZ_CAMEL_CONTEXT + "-" + getCamelContext().getName(), getCamelContext());
152 }
153
154 // if not configure to auto start then don't start it
155 if (!isAutoStartScheduler()) {
156 LOG.info("QuartzComponent configured to not auto start Quartz scheduler.");
157 return;
158 }
159
160 // only start scheduler when CamelContext have finished starting
161 startScheduler();
162 }
163
164 @Override
165 protected void doStart() throws Exception {
166 super.doStart();
167 if (scheduler == null) {
168 scheduler = getScheduler();
169 }
170 }
171
172 @Override
173 protected void doStop() throws Exception {
174 super.doStop();
175
176 if (scheduler != null) {
177 int number = JOBS.get();
178 if (number > 0) {
179 LOG.info("Cannot shutdown Quartz scheduler: " + scheduler.getSchedulerName() + " as there are still " + number + " jobs registered.");
180 } else {
181 // no more jobs then shutdown the scheduler
182 LOG.info("There are no more jobs registered, so shutting down Quartz scheduler: " + scheduler.getSchedulerName());
183 scheduler.shutdown();
184 scheduler = null;
185 }
186 }
187 }
188
189 public void addJob(JobDetail job, Trigger trigger) throws SchedulerException {
190 if (scheduler == null) {
191 // add job to internal list because we will defer adding to the scheduler when camel context has been fully started
192 jobsToAdd.add(new JobToAdd(job, trigger));
193 } else {
194 // add job directly to scheduler
195 doAddJob(job, trigger);
196 }
197 }
198
199 private void doAddJob(JobDetail job, Trigger trigger) throws SchedulerException {
200 JOBS.incrementAndGet();
201
202 Trigger existingTrigger = getScheduler().getTrigger(trigger.getName(), trigger.getGroup());
203 if (existingTrigger == null) {
204 if (LOG.isDebugEnabled()) {
205 LOG.debug("Adding job using trigger: " + trigger.getGroup() + "/" + trigger.getName());
206 }
207 getScheduler().scheduleJob(job, trigger);
208 } else if (hasTriggerChanged(existingTrigger, trigger)) {
209 if (LOG.isDebugEnabled()) {
210 LOG.debug("Trigger: " + trigger.getGroup() + "/" + trigger.getName() + " already exists and will be updated by Quartz.");
211 }
212 scheduler.addJob(job, true);
213 trigger.setJobName(job.getName());
214 scheduler.rescheduleJob(trigger.getName(), trigger.getGroup(), trigger);
215 } else {
216 if (LOG.isDebugEnabled()) {
217 LOG.debug("Trigger: " + trigger.getGroup() + "/" + trigger.getName() + " already exists and will be resumed automatically by Quartz.");
218 }
219 if (!isClustered()) {
220 scheduler.resumeTrigger(trigger.getName(), trigger.getGroup());
221 }
222 }
223 }
224
225 private boolean hasTriggerChanged(Trigger oldTrigger, Trigger newTrigger) {
226 if (oldTrigger instanceof CronTrigger && oldTrigger.equals(newTrigger)) {
227 CronTrigger oldCron = (CronTrigger) oldTrigger;
228 CronTrigger newCron = (CronTrigger) newTrigger;
229 return !oldCron.getCronExpression().equals(newCron.getCronExpression());
230 } else {
231 return !newTrigger.equals(oldTrigger);
232 }
233 }
234
235 public void removeJob(JobDetail job, Trigger trigger) throws SchedulerException {
236 JOBS.decrementAndGet();
237
238 if (isClustered()) {
239 // do not remove jobs which are clustered, as we want the jobs to continue running on the other nodes
240 if (LOG.isDebugEnabled()) {
241 LOG.debug("Cannot removing job using trigger: " + trigger.getGroup() + "/" + trigger.getName() + " as the JobStore is clustered.");
242 }
243 return;
244 }
245
246 // only unschedule volatile jobs
247 if (job.isVolatile()) {
248 if (LOG.isDebugEnabled()) {
249 LOG.debug("Removing job using trigger: " + trigger.getGroup() + "/" + trigger.getName());
250 }
251 getScheduler().unscheduleJob(trigger.getName(), trigger.getGroup());
252 } else {
253 // but pause jobs so we can resume them if the application restarts
254 if (LOG.isDebugEnabled()) {
255 LOG.debug("Pausing job using trigger: " + trigger.getGroup() + "/" + trigger.getName());
256 }
257 getScheduler().pauseTrigger(trigger.getName(), trigger.getGroup());
258 }
259 }
260
261 /**
262 * To force shutdown the quartz scheduler
263 *
264 * @throws SchedulerException can be thrown if error shutting down
265 */
266 public void shutdownScheduler() throws SchedulerException {
267 if (scheduler != null) {
268 LOG.info("Forcing shutdown of Quartz scheduler: " + scheduler.getSchedulerName());
269 scheduler.shutdown();
270 scheduler = null;
271 }
272 }
273
274 /**
275 * Is the quartz scheduler clustered?
276 */
277 public boolean isClustered() throws SchedulerException {
278 return getScheduler().getMetaData().isJobStoreClustered();
279 }
280
281 /**
282 * To force starting the quartz scheduler
283 *
284 * @throws SchedulerException can be thrown if error starting
285 */
286 public void startScheduler() throws SchedulerException {
287 for (JobToAdd add : jobsToAdd) {
288 doAddJob(add.getJob(), add.getTrigger());
289 }
290 jobsToAdd.clear();
291
292 if (!getScheduler().isStarted()) {
293 if (getStartDelayedSeconds() > 0) {
294 LOG.info("Starting Quartz scheduler: " + getScheduler().getSchedulerName() + " delayed: " + getStartDelayedSeconds() + " seconds.");
295 getScheduler().startDelayed(getStartDelayedSeconds());
296 } else {
297 LOG.info("Starting Quartz scheduler: " + getScheduler().getSchedulerName());
298 getScheduler().start();
299 }
300 }
301 }
302
303 // Properties
304 // -------------------------------------------------------------------------
305
306 public SchedulerFactory getFactory() throws SchedulerException {
307 if (factory == null) {
308 factory = createSchedulerFactory();
309 }
310 return factory;
311 }
312
313 public void setFactory(final SchedulerFactory factory) {
314 this.factory = factory;
315 }
316
317 public synchronized Scheduler getScheduler() throws SchedulerException {
318 if (scheduler == null) {
319 scheduler = createScheduler();
320 }
321 return scheduler;
322 }
323
324 public void setScheduler(final Scheduler scheduler) {
325 QuartzComponent.scheduler = scheduler;
326 }
327
328 public Properties getProperties() {
329 return properties;
330 }
331
332 public void setProperties(Properties properties) {
333 this.properties = properties;
334 }
335
336 public String getPropertiesFile() {
337 return propertiesFile;
338 }
339
340 public void setPropertiesFile(String propertiesFile) {
341 this.propertiesFile = propertiesFile;
342 }
343
344 public int getStartDelayedSeconds() {
345 return startDelayedSeconds;
346 }
347
348 public void setStartDelayedSeconds(int startDelayedSeconds) {
349 this.startDelayedSeconds = startDelayedSeconds;
350 }
351
352 public boolean isAutoStartScheduler() {
353 return autoStartScheduler;
354 }
355
356 public void setAutoStartScheduler(boolean autoStartScheduler) {
357 this.autoStartScheduler = autoStartScheduler;
358 }
359
360 // Implementation methods
361 // -------------------------------------------------------------------------
362
363 protected Properties loadProperties() throws SchedulerException {
364 Properties answer = getProperties();
365 if (answer == null && getPropertiesFile() != null) {
366 if (LOG.isInfoEnabled()) {
367 LOG.info("Loading Quartz properties file from classpath: " + getPropertiesFile());
368 }
369 InputStream is = getCamelContext().getClassResolver().loadResourceAsStream(getPropertiesFile());
370 if (is == null) {
371 throw new SchedulerException("Quartz properties file not found in classpath: " + getPropertiesFile());
372 }
373 answer = new Properties();
374 try {
375 answer.load(is);
376 } catch (IOException e) {
377 throw new SchedulerException("Error loading Quartz properties file from classpath: " + getPropertiesFile(), e);
378 }
379 }
380 return answer;
381 }
382
383 protected SchedulerFactory createSchedulerFactory() throws SchedulerException {
384 Properties prop = loadProperties();
385 if (prop != null) {
386 if (LOG.isDebugEnabled()) {
387 LOG.debug("Creating SchedulerFactory with properties: " + prop);
388 }
389 return new StdSchedulerFactory(prop);
390 } else {
391 return new StdSchedulerFactory();
392 }
393 }
394
395 protected Scheduler createScheduler() throws SchedulerException {
396 Scheduler scheduler = getFactory().getScheduler();
397 // register current camel context to scheduler so we can look it up when jobs is being triggered
398 scheduler.getContext().put(QuartzConstants.QUARTZ_CAMEL_CONTEXT + "-" + getCamelContext().getName(), getCamelContext());
399 return scheduler;
400 }
401
402 }