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    }