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}