001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.scheduling.quartz;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027import org.quartz.Calendar;
028import org.quartz.JobDetail;
029import org.quartz.JobListener;
030import org.quartz.ListenerManager;
031import org.quartz.ObjectAlreadyExistsException;
032import org.quartz.Scheduler;
033import org.quartz.SchedulerException;
034import org.quartz.SchedulerListener;
035import org.quartz.Trigger;
036import org.quartz.TriggerListener;
037import org.quartz.spi.ClassLoadHelper;
038import org.quartz.xml.XMLSchedulingDataProcessor;
039
040import org.springframework.context.ResourceLoaderAware;
041import org.springframework.core.io.ResourceLoader;
042import org.springframework.transaction.PlatformTransactionManager;
043import org.springframework.transaction.TransactionException;
044import org.springframework.transaction.TransactionStatus;
045import org.springframework.transaction.support.DefaultTransactionDefinition;
046
047/**
048 * Common base class for accessing a Quartz Scheduler, i.e. for registering jobs,
049 * triggers and listeners on a {@link org.quartz.Scheduler} instance.
050 *
051 * <p>For concrete usage, check out the {@link SchedulerFactoryBean} and
052 * {@link SchedulerAccessorBean} classes.
053 *
054 * <p>Compatible with Quartz 2.1.4 and higher, as of Spring 4.1.
055 *
056 * @author Juergen Hoeller
057 * @author Stephane Nicoll
058 * @since 2.5.6
059 */
060public abstract class SchedulerAccessor implements ResourceLoaderAware {
061
062        protected final Log logger = LogFactory.getLog(getClass());
063
064        private boolean overwriteExistingJobs = false;
065
066        private String[] jobSchedulingDataLocations;
067
068        private List<JobDetail> jobDetails;
069
070        private Map<String, Calendar> calendars;
071
072        private List<Trigger> triggers;
073
074        private SchedulerListener[] schedulerListeners;
075
076        private JobListener[] globalJobListeners;
077
078        private TriggerListener[] globalTriggerListeners;
079
080        private PlatformTransactionManager transactionManager;
081
082        protected ResourceLoader resourceLoader;
083
084
085        /**
086         * Set whether any jobs defined on this SchedulerFactoryBean should overwrite
087         * existing job definitions. Default is "false", to not overwrite already
088         * registered jobs that have been read in from a persistent job store.
089         */
090        public void setOverwriteExistingJobs(boolean overwriteExistingJobs) {
091                this.overwriteExistingJobs = overwriteExistingJobs;
092        }
093
094        /**
095         * Set the location of a Quartz job definition XML file that follows the
096         * "job_scheduling_data_1_5" XSD or better. Can be specified to automatically
097         * register jobs that are defined in such a file, possibly in addition
098         * to jobs defined directly on this SchedulerFactoryBean.
099         * @see org.quartz.xml.XMLSchedulingDataProcessor
100         */
101        public void setJobSchedulingDataLocation(String jobSchedulingDataLocation) {
102                this.jobSchedulingDataLocations = new String[] {jobSchedulingDataLocation};
103        }
104
105        /**
106         * Set the locations of Quartz job definition XML files that follow the
107         * "job_scheduling_data_1_5" XSD or better. Can be specified to automatically
108         * register jobs that are defined in such files, possibly in addition
109         * to jobs defined directly on this SchedulerFactoryBean.
110         * @see org.quartz.xml.XMLSchedulingDataProcessor
111         */
112        public void setJobSchedulingDataLocations(String... jobSchedulingDataLocations) {
113                this.jobSchedulingDataLocations = jobSchedulingDataLocations;
114        }
115
116        /**
117         * Register a list of JobDetail objects with the Scheduler that
118         * this FactoryBean creates, to be referenced by Triggers.
119         * <p>This is not necessary when a Trigger determines the JobDetail
120         * itself: In this case, the JobDetail will be implicitly registered
121         * in combination with the Trigger.
122         * @see #setTriggers
123         * @see org.quartz.JobDetail
124         */
125        public void setJobDetails(JobDetail... jobDetails) {
126                // Use modifiable ArrayList here, to allow for further adding of
127                // JobDetail objects during autodetection of JobDetail-aware Triggers.
128                this.jobDetails = new ArrayList<JobDetail>(Arrays.asList(jobDetails));
129        }
130
131        /**
132         * Register a list of Quartz Calendar objects with the Scheduler
133         * that this FactoryBean creates, to be referenced by Triggers.
134         * @param calendars a Map with calendar names as keys as Calendar
135         * objects as values
136         * @see org.quartz.Calendar
137         */
138        public void setCalendars(Map<String, Calendar> calendars) {
139                this.calendars = calendars;
140        }
141
142        /**
143         * Register a list of Trigger objects with the Scheduler that
144         * this FactoryBean creates.
145         * <p>If the Trigger determines the corresponding JobDetail itself,
146         * the job will be automatically registered with the Scheduler.
147         * Else, the respective JobDetail needs to be registered via the
148         * "jobDetails" property of this FactoryBean.
149         * @see #setJobDetails
150         * @see org.quartz.JobDetail
151         */
152        public void setTriggers(Trigger... triggers) {
153                this.triggers = Arrays.asList(triggers);
154        }
155
156        /**
157         * Specify Quartz SchedulerListeners to be registered with the Scheduler.
158         */
159        public void setSchedulerListeners(SchedulerListener... schedulerListeners) {
160                this.schedulerListeners = schedulerListeners;
161        }
162
163        /**
164         * Specify global Quartz JobListeners to be registered with the Scheduler.
165         * Such JobListeners will apply to all Jobs in the Scheduler.
166         */
167        public void setGlobalJobListeners(JobListener... globalJobListeners) {
168                this.globalJobListeners = globalJobListeners;
169        }
170
171        /**
172         * Specify global Quartz TriggerListeners to be registered with the Scheduler.
173         * Such TriggerListeners will apply to all Triggers in the Scheduler.
174         */
175        public void setGlobalTriggerListeners(TriggerListener... globalTriggerListeners) {
176                this.globalTriggerListeners = globalTriggerListeners;
177        }
178
179        /**
180         * Set the transaction manager to be used for registering jobs and triggers
181         * that are defined by this SchedulerFactoryBean. Default is none; setting
182         * this only makes sense when specifying a DataSource for the Scheduler.
183         */
184        public void setTransactionManager(PlatformTransactionManager transactionManager) {
185                this.transactionManager = transactionManager;
186        }
187
188        @Override
189        public void setResourceLoader(ResourceLoader resourceLoader) {
190                this.resourceLoader = resourceLoader;
191        }
192
193
194        /**
195         * Register jobs and triggers (within a transaction, if possible).
196         */
197        protected void registerJobsAndTriggers() throws SchedulerException {
198                TransactionStatus transactionStatus = null;
199                if (this.transactionManager != null) {
200                        transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
201                }
202
203                try {
204                        if (this.jobSchedulingDataLocations != null) {
205                                ClassLoadHelper clh = new ResourceLoaderClassLoadHelper(this.resourceLoader);
206                                clh.initialize();
207                                XMLSchedulingDataProcessor dataProcessor = new XMLSchedulingDataProcessor(clh);
208                                for (String location : this.jobSchedulingDataLocations) {
209                                        dataProcessor.processFileAndScheduleJobs(location, getScheduler());
210                                }
211                        }
212
213                        // Register JobDetails.
214                        if (this.jobDetails != null) {
215                                for (JobDetail jobDetail : this.jobDetails) {
216                                        addJobToScheduler(jobDetail);
217                                }
218                        }
219                        else {
220                                // Create empty list for easier checks when registering triggers.
221                                this.jobDetails = new LinkedList<JobDetail>();
222                        }
223
224                        // Register Calendars.
225                        if (this.calendars != null) {
226                                for (String calendarName : this.calendars.keySet()) {
227                                        Calendar calendar = this.calendars.get(calendarName);
228                                        getScheduler().addCalendar(calendarName, calendar, true, true);
229                                }
230                        }
231
232                        // Register Triggers.
233                        if (this.triggers != null) {
234                                for (Trigger trigger : this.triggers) {
235                                        addTriggerToScheduler(trigger);
236                                }
237                        }
238                }
239
240                catch (Throwable ex) {
241                        if (transactionStatus != null) {
242                                try {
243                                        this.transactionManager.rollback(transactionStatus);
244                                }
245                                catch (TransactionException tex) {
246                                        logger.error("Job registration exception overridden by rollback exception", ex);
247                                        throw tex;
248                                }
249                        }
250                        if (ex instanceof SchedulerException) {
251                                throw (SchedulerException) ex;
252                        }
253                        if (ex instanceof Exception) {
254                                throw new SchedulerException("Registration of jobs and triggers failed: " + ex.getMessage(), ex);
255                        }
256                        throw new SchedulerException("Registration of jobs and triggers failed: " + ex.getMessage());
257                }
258
259                if (transactionStatus != null) {
260                        this.transactionManager.commit(transactionStatus);
261                }
262        }
263
264        /**
265         * Add the given job to the Scheduler, if it doesn't already exist.
266         * Overwrites the job in any case if "overwriteExistingJobs" is set.
267         * @param jobDetail the job to add
268         * @return {@code true} if the job was actually added,
269         * {@code false} if it already existed before
270         * @see #setOverwriteExistingJobs
271         */
272        private boolean addJobToScheduler(JobDetail jobDetail) throws SchedulerException {
273                if (this.overwriteExistingJobs || getScheduler().getJobDetail(jobDetail.getKey()) == null) {
274                        getScheduler().addJob(jobDetail, true);
275                        return true;
276                }
277                else {
278                        return false;
279                }
280        }
281
282        /**
283         * Add the given trigger to the Scheduler, if it doesn't already exist.
284         * Overwrites the trigger in any case if "overwriteExistingJobs" is set.
285         * @param trigger the trigger to add
286         * @return {@code true} if the trigger was actually added,
287         * {@code false} if it already existed before
288         * @see #setOverwriteExistingJobs
289         */
290        private boolean addTriggerToScheduler(Trigger trigger) throws SchedulerException {
291                boolean triggerExists = (getScheduler().getTrigger(trigger.getKey()) != null);
292                if (triggerExists && !this.overwriteExistingJobs) {
293                        return false;
294                }
295
296                // Check if the Trigger is aware of an associated JobDetail.
297                JobDetail jobDetail = (JobDetail) trigger.getJobDataMap().remove("jobDetail");
298                if (triggerExists) {
299                        if (jobDetail != null && !this.jobDetails.contains(jobDetail) && addJobToScheduler(jobDetail)) {
300                                this.jobDetails.add(jobDetail);
301                        }
302                        try {
303                                getScheduler().rescheduleJob(trigger.getKey(), trigger);
304                        }
305                        catch (ObjectAlreadyExistsException ex) {
306                                if (logger.isDebugEnabled()) {
307                                        logger.debug("Unexpectedly encountered existing trigger on rescheduling, assumably due to " +
308                                                        "cluster race condition: " + ex.getMessage() + " - can safely be ignored");
309                                }
310                        }
311                }
312                else {
313                        try {
314                                if (jobDetail != null && !this.jobDetails.contains(jobDetail) &&
315                                                (this.overwriteExistingJobs || getScheduler().getJobDetail(jobDetail.getKey()) == null)) {
316                                        getScheduler().scheduleJob(jobDetail, trigger);
317                                        this.jobDetails.add(jobDetail);
318                                }
319                                else {
320                                        getScheduler().scheduleJob(trigger);
321                                }
322                        }
323                        catch (ObjectAlreadyExistsException ex) {
324                                if (logger.isDebugEnabled()) {
325                                        logger.debug("Unexpectedly encountered existing trigger on job scheduling, assumably due to " +
326                                                        "cluster race condition: " + ex.getMessage() + " - can safely be ignored");
327                                }
328                                if (this.overwriteExistingJobs) {
329                                        getScheduler().rescheduleJob(trigger.getKey(), trigger);
330                                }
331                        }
332                }
333                return true;
334        }
335
336        /**
337         * Register all specified listeners with the Scheduler.
338         */
339        protected void registerListeners() throws SchedulerException {
340                ListenerManager listenerManager = getScheduler().getListenerManager();
341                if (this.schedulerListeners != null) {
342                        for (SchedulerListener listener : this.schedulerListeners) {
343                                listenerManager.addSchedulerListener(listener);
344                        }
345                }
346                if (this.globalJobListeners != null) {
347                        for (JobListener listener : this.globalJobListeners) {
348                                listenerManager.addJobListener(listener);
349                        }
350                }
351                if (this.globalTriggerListeners != null) {
352                        for (TriggerListener listener : this.globalTriggerListeners) {
353                                listenerManager.addTriggerListener(listener);
354                        }
355                }
356        }
357
358
359        /**
360         * Template method that determines the Scheduler to operate on.
361         * To be implemented by subclasses.
362         */
363        protected abstract Scheduler getScheduler();
364
365}