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    }