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    }