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    }