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