1 /*
2  * Copyright 2002-2011 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.springframework.scheduling.quartz;
18 
19 import java.lang.reflect.Method;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.LinkedList;
23 import java.util.List;
24 import java.util.Map;
25 
26 import org.apache.commons.logging.Log;
27 import org.apache.commons.logging.LogFactory;
28 import org.quartz.Calendar;
29 import org.quartz.JobDetail;
30 import org.quartz.JobListener;
31 import org.quartz.ObjectAlreadyExistsException;
32 import org.quartz.Scheduler;
33 import org.quartz.SchedulerException;
34 import org.quartz.SchedulerListener;
35 import org.quartz.Trigger;
36 import org.quartz.TriggerListener;
37 import org.quartz.spi.ClassLoadHelper;
38 
39 import org.springframework.context.ResourceLoaderAware;
40 import org.springframework.core.io.ResourceLoader;
41 import org.springframework.transaction.PlatformTransactionManager;
42 import org.springframework.transaction.TransactionException;
43 import org.springframework.transaction.TransactionStatus;
44 import org.springframework.transaction.support.DefaultTransactionDefinition;
45 import org.springframework.util.ReflectionUtils;
46 
47 /**
48  * Common base class for accessing a Quartz Scheduler, i.e. for registering jobs,
49  * triggers and listeners on a {@link org.quartz.Scheduler} instance.
50  *
51  * <p>For concrete usage, check out the {@link SchedulerFactoryBean} and
52  * {@link SchedulerAccessorBean} classes.
53  *
54  * <p>Compatible with Quartz 1.5+ as well as Quartz 2.0/2.1, as of Spring 3.1.
55  *
56  * @author Juergen Hoeller
57  * @since 2.5.6
58  */
59 public abstract class SchedulerAccessor implements ResourceLoaderAware {
60 
61 	private static Class<?> jobKeyClass;
62 
63 	private static Class<?> triggerKeyClass;
64 
65 	static {
66 		try {
67 			jobKeyClass = Class.forName("org.quartz.JobKey");
68 			triggerKeyClass = Class.forName("org.quartz.TriggerKey");
69 		}
70 		catch (ClassNotFoundException ex) {
71 			jobKeyClass = null;
72 			triggerKeyClass = null;
73 		}
74 	}
75 
76 
77 	protected final Log logger = LogFactory.getLog(getClass());
78 
79 	private boolean overwriteExistingJobs = false;
80 
81 	private String[] jobSchedulingDataLocations;
82 
83 	private List<JobDetail> jobDetails;
84 
85 	private Map<String, Calendar> calendars;
86 
87 	private List<Trigger> triggers;
88 
89 	private SchedulerListener[] schedulerListeners;
90 
91 	private JobListener[] globalJobListeners;
92 
93 	private JobListener[] jobListeners;
94 
95 	private TriggerListener[] globalTriggerListeners;
96 
97 	private TriggerListener[] triggerListeners;
98 
99 	private PlatformTransactionManager transactionManager;
100 
101 	protected ResourceLoader resourceLoader;
102 
103 
104 	/**
105 	 * Set whether any jobs defined on this SchedulerFactoryBean should overwrite
106 	 * existing job definitions. Default is "false", to not overwrite already
107 	 * registered jobs that have been read in from a persistent job store.
108 	 */
setOverwriteExistingJobs(boolean overwriteExistingJobs)109 	public void setOverwriteExistingJobs(boolean overwriteExistingJobs) {
110 		this.overwriteExistingJobs = overwriteExistingJobs;
111 	}
112 
113 	/**
114 	 * Set the location of a Quartz job definition XML file that follows the
115 	 * "job_scheduling_data_1_5" XSD. Can be specified to automatically
116 	 * register jobs that are defined in such a file, possibly in addition
117 	 * to jobs defined directly on this SchedulerFactoryBean.
118 	 * @see org.quartz.xml.JobSchedulingDataProcessor
119 	 */
setJobSchedulingDataLocation(String jobSchedulingDataLocation)120 	public void setJobSchedulingDataLocation(String jobSchedulingDataLocation) {
121 		this.jobSchedulingDataLocations = new String[] {jobSchedulingDataLocation};
122 	}
123 
124 	/**
125 	 * Set the locations of Quartz job definition XML files that follow the
126 	 * "job_scheduling_data_1_5" XSD. Can be specified to automatically
127 	 * register jobs that are defined in such files, possibly in addition
128 	 * to jobs defined directly on this SchedulerFactoryBean.
129 	 * @see org.quartz.xml.JobSchedulingDataProcessor
130 	 */
setJobSchedulingDataLocations(String[] jobSchedulingDataLocations)131 	public void setJobSchedulingDataLocations(String[] jobSchedulingDataLocations) {
132 		this.jobSchedulingDataLocations = jobSchedulingDataLocations;
133 	}
134 
135 	/**
136 	 * Register a list of JobDetail objects with the Scheduler that
137 	 * this FactoryBean creates, to be referenced by Triggers.
138 	 * <p>This is not necessary when a Trigger determines the JobDetail
139 	 * itself: In this case, the JobDetail will be implicitly registered
140 	 * in combination with the Trigger.
141 	 * @see #setTriggers
142 	 * @see org.quartz.JobDetail
143 	 * @see JobDetailBean
144 	 * @see JobDetailAwareTrigger
145 	 * @see org.quartz.Trigger#setJobName
146 	 */
setJobDetails(JobDetail[] jobDetails)147 	public void setJobDetails(JobDetail[] jobDetails) {
148 		// Use modifiable ArrayList here, to allow for further adding of
149 		// JobDetail objects during autodetection of JobDetailAwareTriggers.
150 		this.jobDetails = new ArrayList<JobDetail>(Arrays.asList(jobDetails));
151 	}
152 
153 	/**
154 	 * Register a list of Quartz Calendar objects with the Scheduler
155 	 * that this FactoryBean creates, to be referenced by Triggers.
156 	 * @param calendars Map with calendar names as keys as Calendar
157 	 * objects as values
158 	 * @see org.quartz.Calendar
159 	 * @see org.quartz.Trigger#setCalendarName
160 	 */
setCalendars(Map<String, Calendar> calendars)161 	public void setCalendars(Map<String, Calendar> calendars) {
162 		this.calendars = calendars;
163 	}
164 
165 	/**
166 	 * Register a list of Trigger objects with the Scheduler that
167 	 * this FactoryBean creates.
168 	 * <p>If the Trigger determines the corresponding JobDetail itself,
169 	 * the job will be automatically registered with the Scheduler.
170 	 * Else, the respective JobDetail needs to be registered via the
171 	 * "jobDetails" property of this FactoryBean.
172 	 * @see #setJobDetails
173 	 * @see org.quartz.JobDetail
174 	 * @see JobDetailAwareTrigger
175 	 * @see CronTriggerBean
176 	 * @see SimpleTriggerBean
177 	 */
setTriggers(Trigger[] triggers)178 	public void setTriggers(Trigger[] triggers) {
179 		this.triggers = Arrays.asList(triggers);
180 	}
181 
182 
183 	/**
184 	 * Specify Quartz SchedulerListeners to be registered with the Scheduler.
185 	 */
setSchedulerListeners(SchedulerListener[] schedulerListeners)186 	public void setSchedulerListeners(SchedulerListener[] schedulerListeners) {
187 		this.schedulerListeners = schedulerListeners;
188 	}
189 
190 	/**
191 	 * Specify global Quartz JobListeners to be registered with the Scheduler.
192 	 * Such JobListeners will apply to all Jobs in the Scheduler.
193 	 */
setGlobalJobListeners(JobListener[] globalJobListeners)194 	public void setGlobalJobListeners(JobListener[] globalJobListeners) {
195 		this.globalJobListeners = globalJobListeners;
196 	}
197 
198 	/**
199 	 * Specify named Quartz JobListeners to be registered with the Scheduler.
200 	 * Such JobListeners will only apply to Jobs that explicitly activate
201 	 * them via their name.
202 	 * @see org.quartz.JobListener#getName
203 	 * @see org.quartz.JobDetail#addJobListener
204 	 * @see JobDetailBean#setJobListenerNames
205 	 */
setJobListeners(JobListener[] jobListeners)206 	public void setJobListeners(JobListener[] jobListeners) {
207 		this.jobListeners = jobListeners;
208 	}
209 
210 	/**
211 	 * Specify global Quartz TriggerListeners to be registered with the Scheduler.
212 	 * Such TriggerListeners will apply to all Triggers in the Scheduler.
213 	 */
setGlobalTriggerListeners(TriggerListener[] globalTriggerListeners)214 	public void setGlobalTriggerListeners(TriggerListener[] globalTriggerListeners) {
215 		this.globalTriggerListeners = globalTriggerListeners;
216 	}
217 
218 	/**
219 	 * Specify named Quartz TriggerListeners to be registered with the Scheduler.
220 	 * Such TriggerListeners will only apply to Triggers that explicitly activate
221 	 * them via their name.
222 	 * @see org.quartz.TriggerListener#getName
223 	 * @see org.quartz.Trigger#addTriggerListener
224 	 * @see CronTriggerBean#setTriggerListenerNames
225 	 * @see SimpleTriggerBean#setTriggerListenerNames
226 	 */
setTriggerListeners(TriggerListener[] triggerListeners)227 	public void setTriggerListeners(TriggerListener[] triggerListeners) {
228 		this.triggerListeners = triggerListeners;
229 	}
230 
231 
232 	/**
233 	 * Set the transaction manager to be used for registering jobs and triggers
234 	 * that are defined by this SchedulerFactoryBean. Default is none; setting
235 	 * this only makes sense when specifying a DataSource for the Scheduler.
236 	 */
setTransactionManager(PlatformTransactionManager transactionManager)237 	public void setTransactionManager(PlatformTransactionManager transactionManager) {
238 		this.transactionManager = transactionManager;
239 	}
240 
setResourceLoader(ResourceLoader resourceLoader)241 	public void setResourceLoader(ResourceLoader resourceLoader) {
242 		this.resourceLoader = resourceLoader;
243 	}
244 
245 
246 	/**
247 	 * Register jobs and triggers (within a transaction, if possible).
248 	 */
registerJobsAndTriggers()249 	protected void registerJobsAndTriggers() throws SchedulerException {
250 		TransactionStatus transactionStatus = null;
251 		if (this.transactionManager != null) {
252 			transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
253 		}
254 		try {
255 
256 			if (this.jobSchedulingDataLocations != null) {
257 				ClassLoadHelper clh = new ResourceLoaderClassLoadHelper(this.resourceLoader);
258 				clh.initialize();
259 				try {
260 					// Quartz 1.8 or higher?
261 					Class dataProcessorClass = getClass().getClassLoader().loadClass("org.quartz.xml.XMLSchedulingDataProcessor");
262 					logger.debug("Using Quartz 1.8 XMLSchedulingDataProcessor");
263 					Object dataProcessor = dataProcessorClass.getConstructor(ClassLoadHelper.class).newInstance(clh);
264 					Method processFileAndScheduleJobs = dataProcessorClass.getMethod("processFileAndScheduleJobs", String.class, Scheduler.class);
265 					for (String location : this.jobSchedulingDataLocations) {
266 						processFileAndScheduleJobs.invoke(dataProcessor, location, getScheduler());
267 					}
268 				}
269 				catch (ClassNotFoundException ex) {
270 					// Quartz 1.6
271 					Class dataProcessorClass = getClass().getClassLoader().loadClass("org.quartz.xml.JobSchedulingDataProcessor");
272 					logger.debug("Using Quartz 1.6 JobSchedulingDataProcessor");
273 					Object dataProcessor = dataProcessorClass.getConstructor(ClassLoadHelper.class, boolean.class, boolean.class).newInstance(clh, true, true);
274 					Method processFileAndScheduleJobs = dataProcessorClass.getMethod("processFileAndScheduleJobs", String.class, Scheduler.class, boolean.class);
275 					for (String location : this.jobSchedulingDataLocations) {
276 						processFileAndScheduleJobs.invoke(dataProcessor, location, getScheduler(), this.overwriteExistingJobs);
277 					}
278 				}
279 			}
280 
281 			// Register JobDetails.
282 			if (this.jobDetails != null) {
283 				for (JobDetail jobDetail : this.jobDetails) {
284 					addJobToScheduler(jobDetail);
285 				}
286 			}
287 			else {
288 				// Create empty list for easier checks when registering triggers.
289 				this.jobDetails = new LinkedList<JobDetail>();
290 			}
291 
292 			// Register Calendars.
293 			if (this.calendars != null) {
294 				for (String calendarName : this.calendars.keySet()) {
295 					Calendar calendar = this.calendars.get(calendarName);
296 					getScheduler().addCalendar(calendarName, calendar, true, true);
297 				}
298 			}
299 
300 			// Register Triggers.
301 			if (this.triggers != null) {
302 				for (Trigger trigger : this.triggers) {
303 					addTriggerToScheduler(trigger);
304 				}
305 			}
306 		}
307 
308 		catch (Throwable ex) {
309 			if (transactionStatus != null) {
310 				try {
311 					this.transactionManager.rollback(transactionStatus);
312 				}
313 				catch (TransactionException tex) {
314 					logger.error("Job registration exception overridden by rollback exception", ex);
315 					throw tex;
316 				}
317 			}
318 			if (ex instanceof SchedulerException) {
319 				throw (SchedulerException) ex;
320 			}
321 			if (ex instanceof Exception) {
322 				throw new SchedulerException("Registration of jobs and triggers failed: " + ex.getMessage(), ex);
323 			}
324 			throw new SchedulerException("Registration of jobs and triggers failed: " + ex.getMessage());
325 		}
326 
327 		if (transactionStatus != null) {
328 			this.transactionManager.commit(transactionStatus);
329 		}
330 	}
331 
332 	/**
333 	 * Add the given job to the Scheduler, if it doesn't already exist.
334 	 * Overwrites the job in any case if "overwriteExistingJobs" is set.
335 	 * @param jobDetail the job to add
336 	 * @return <code>true</code> if the job was actually added,
337 	 * <code>false</code> if it already existed before
338 	 * @see #setOverwriteExistingJobs
339 	 */
addJobToScheduler(JobDetail jobDetail)340 	private boolean addJobToScheduler(JobDetail jobDetail) throws SchedulerException {
341 		if (this.overwriteExistingJobs || !jobDetailExists(jobDetail)) {
342 			getScheduler().addJob(jobDetail, true);
343 			return true;
344 		}
345 		else {
346 			return false;
347 		}
348 	}
349 
350 	/**
351 	 * Add the given trigger to the Scheduler, if it doesn't already exist.
352 	 * Overwrites the trigger in any case if "overwriteExistingJobs" is set.
353 	 * @param trigger the trigger to add
354 	 * @return <code>true</code> if the trigger was actually added,
355 	 * <code>false</code> if it already existed before
356 	 * @see #setOverwriteExistingJobs
357 	 */
addTriggerToScheduler(Trigger trigger)358 	private boolean addTriggerToScheduler(Trigger trigger) throws SchedulerException {
359 		boolean triggerExists = triggerExists(trigger);
360 		if (!triggerExists || this.overwriteExistingJobs) {
361 			// Check if the Trigger is aware of an associated JobDetail.
362 			JobDetail jobDetail = findJobDetail(trigger);
363 			if (jobDetail != null) {
364 				// Automatically register the JobDetail too.
365 				if (!this.jobDetails.contains(jobDetail) && addJobToScheduler(jobDetail)) {
366 					this.jobDetails.add(jobDetail);
367 				}
368 			}
369 			if (!triggerExists) {
370 				try {
371 					getScheduler().scheduleJob(trigger);
372 				}
373 				catch (ObjectAlreadyExistsException ex) {
374 					if (logger.isDebugEnabled()) {
375 						logger.debug("Unexpectedly found existing trigger, assumably due to cluster race condition: " +
376 								ex.getMessage() + " - can safely be ignored");
377 					}
378 					if (this.overwriteExistingJobs) {
379 						rescheduleJob(trigger);
380 					}
381 				}
382 			}
383 			else {
384 				rescheduleJob(trigger);
385 			}
386 			return true;
387 		}
388 		else {
389 			return false;
390 		}
391 	}
392 
findJobDetail(Trigger trigger)393 	private JobDetail findJobDetail(Trigger trigger) {
394 		if (trigger instanceof JobDetailAwareTrigger) {
395 			return ((JobDetailAwareTrigger) trigger).getJobDetail();
396 		}
397 		else {
398 			try {
399 				Map jobDataMap = (Map) ReflectionUtils.invokeMethod(Trigger.class.getMethod("getJobDataMap"), trigger);
400 				return (JobDetail) jobDataMap.get(JobDetailAwareTrigger.JOB_DETAIL_KEY);
401 			}
402 			catch (NoSuchMethodException ex) {
403 				throw new IllegalStateException("Inconsistent Quartz API: " + ex);
404 			}
405 		}
406 	}
407 
408 
409 	// Reflectively adapting to differences between Quartz 1.x and Quartz 2.0...
jobDetailExists(JobDetail jobDetail)410 	private boolean jobDetailExists(JobDetail jobDetail) throws SchedulerException {
411 		if (jobKeyClass != null) {
412 			try {
413 				Method getJobDetail = Scheduler.class.getMethod("getJobDetail", jobKeyClass);
414 				Object key = ReflectionUtils.invokeMethod(JobDetail.class.getMethod("getKey"), jobDetail);
415 				return (ReflectionUtils.invokeMethod(getJobDetail, getScheduler(), key) != null);
416 			}
417 			catch (NoSuchMethodException ex) {
418 				throw new IllegalStateException("Inconsistent Quartz 2.0 API: " + ex);
419 			}
420 		}
421 		else {
422 			return (getScheduler().getJobDetail(jobDetail.getName(), jobDetail.getGroup()) != null);
423 		}
424 	}
425 
426 	// Reflectively adapting to differences between Quartz 1.x and Quartz 2.0...
triggerExists(Trigger trigger)427 	private boolean triggerExists(Trigger trigger) throws SchedulerException {
428 		if (triggerKeyClass != null) {
429 			try {
430 				Method getTrigger = Scheduler.class.getMethod("getTrigger", triggerKeyClass);
431 				Object key = ReflectionUtils.invokeMethod(Trigger.class.getMethod("getKey"), trigger);
432 				return (ReflectionUtils.invokeMethod(getTrigger, getScheduler(), key) != null);
433 			}
434 			catch (NoSuchMethodException ex) {
435 				throw new IllegalStateException("Inconsistent Quartz 2.0 API: " + ex);
436 			}
437 		}
438 		else {
439 			return (getScheduler().getTrigger(trigger.getName(), trigger.getGroup()) != null);
440 		}
441 	}
442 
443 	// Reflectively adapting to differences between Quartz 1.x and Quartz 2.0...
rescheduleJob(Trigger trigger)444 	private void rescheduleJob(Trigger trigger) throws SchedulerException {
445 		if (triggerKeyClass != null) {
446 			try {
447 				Method rescheduleJob = Scheduler.class.getMethod("rescheduleJob", triggerKeyClass, Trigger.class);
448 				Object key = ReflectionUtils.invokeMethod(Trigger.class.getMethod("getKey"), trigger);
449 				ReflectionUtils.invokeMethod(rescheduleJob, getScheduler(), key, trigger);
450 			}
451 			catch (NoSuchMethodException ex) {
452 				throw new IllegalStateException("Inconsistent Quartz 2.0 API: " + ex);
453 			}
454 		}
455 		else {
456 			getScheduler().rescheduleJob(trigger.getName(), trigger.getGroup(), trigger);
457 		}
458 	}
459 
460 
461 	/**
462 	 * Register all specified listeners with the Scheduler.
463 	 */
registerListeners()464 	protected void registerListeners() throws SchedulerException {
465 		Object target;
466 		boolean quartz2;
467 		try {
468 			Method getListenerManager = Scheduler.class.getMethod("getListenerManager");
469 			target = ReflectionUtils.invokeMethod(getListenerManager, getScheduler());
470 			quartz2 = true;
471 		}
472 		catch (NoSuchMethodException ex) {
473 			target = getScheduler();
474 			quartz2 = false;
475 		}
476 
477 		try {
478 			if (this.schedulerListeners != null) {
479 				Method addSchedulerListener = target.getClass().getMethod("addSchedulerListener", SchedulerListener.class);
480 				for (SchedulerListener listener : this.schedulerListeners) {
481 					ReflectionUtils.invokeMethod(addSchedulerListener, target, listener);
482 				}
483 			}
484 			if (this.globalJobListeners != null) {
485 				Method addJobListener = target.getClass().getMethod(
486 						(quartz2 ? "addJobListener" : "addGlobalJobListener"), JobListener.class);
487 				for (JobListener listener : this.globalJobListeners) {
488 					ReflectionUtils.invokeMethod(addJobListener, target, listener);
489 				}
490 			}
491 			if (this.jobListeners != null) {
492 				for (JobListener listener : this.jobListeners) {
493 					if (quartz2) {
494 						throw new IllegalStateException("Non-global JobListeners not supported on Quartz 2 - " +
495 								"manually register a Matcher against the Quartz ListenerManager instead");
496 					}
497 					getScheduler().addJobListener(listener);
498 				}
499 			}
500 			if (this.globalTriggerListeners != null) {
501 				Method addTriggerListener = target.getClass().getMethod(
502 						(quartz2 ? "addTriggerListener" : "addGlobalTriggerListener"), TriggerListener.class);
503 				for (TriggerListener listener : this.globalTriggerListeners) {
504 					ReflectionUtils.invokeMethod(addTriggerListener, target, listener);
505 				}
506 			}
507 			if (this.triggerListeners != null) {
508 				for (TriggerListener listener : this.triggerListeners) {
509 					if (quartz2) {
510 						throw new IllegalStateException("Non-global TriggerListeners not supported on Quartz 2 - " +
511 								"manually register a Matcher against the Quartz ListenerManager instead");
512 					}
513 					getScheduler().addTriggerListener(listener);
514 				}
515 			}
516 		}
517 		catch (NoSuchMethodException ex) {
518 			throw new IllegalStateException("Expected Quartz API not present: " + ex);
519 		}
520 	}
521 
522 
523 	/**
524 	 * Template method that determines the Scheduler to operate on.
525 	 * To be implemented by subclasses.
526 	 */
getScheduler()527 	protected abstract Scheduler getScheduler();
528 
529 }
530