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