001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.scheduler; 029 030import org.opencms.file.CmsObject; 031import org.opencms.i18n.CmsMessageContainer; 032import org.opencms.main.CmsIllegalArgumentException; 033import org.opencms.main.CmsLog; 034import org.opencms.main.OpenCms; 035import org.opencms.security.CmsRole; 036import org.opencms.security.CmsRoleViolationException; 037import org.opencms.util.CmsStringUtil; 038import org.opencms.util.CmsUUID; 039 040import java.util.ArrayList; 041import java.util.Collections; 042import java.util.Date; 043import java.util.Iterator; 044import java.util.List; 045import java.util.Properties; 046 047import org.apache.commons.logging.Log; 048 049import org.quartz.CronScheduleBuilder; 050import org.quartz.Job; 051import org.quartz.JobBuilder; 052import org.quartz.JobDataMap; 053import org.quartz.JobExecutionContext; 054import org.quartz.Scheduler; 055import org.quartz.SchedulerException; 056import org.quartz.SchedulerFactory; 057import org.quartz.Trigger; 058import org.quartz.TriggerBuilder; 059import org.quartz.TriggerKey; 060import org.quartz.impl.JobDetailImpl; 061import org.quartz.impl.StdSchedulerFactory; 062 063/** 064 * Manages the OpenCms scheduled jobs.<p> 065 * 066 * Please see the documentation of the class {@link org.opencms.scheduler.CmsScheduledJobInfo} 067 * for a full description of the OpenCms scheduling capabilities.<p> 068 * 069 * The OpenCms scheduler implementation internally uses the 070 * <a href="http://www.opensymphony.com/quartz/">Quartz scheduler</a> from 071 * the <a href="http://www.opensymphony.com/">OpenSymphony project</a>.<p> 072 * 073 * This manager class implements the <code>org.quartz.Job</code> interface 074 * and wraps all calls to the {@link org.opencms.scheduler.I_CmsScheduledJob} implementing 075 * classes.<p> 076 * 077 * @since 6.0.0 078 * 079 * @see org.opencms.scheduler.CmsScheduledJobInfo 080 */ 081public class CmsScheduleManager implements Job { 082 083 /** Key for the scheduled job description in the job data map. */ 084 public static final String SCHEDULER_JOB_INFO = "org.opencms.scheduler.CmsScheduledJobInfo"; 085 086 /** The log object for this class. */ 087 private static final Log LOG = CmsLog.getLog(CmsScheduleManager.class); 088 089 /** The Admin context used for creation of users for the individual jobs. */ 090 private CmsObject m_adminCms; 091 092 /** The list of job entries from the configuration. */ 093 private List<CmsScheduledJobInfo> m_configuredJobs; 094 095 /** The list of scheduled jobs. */ 096 private List<CmsScheduledJobInfo> m_jobs; 097 098 /** The initialized scheduler. */ 099 private Scheduler m_scheduler; 100 101 /** 102 * Default constructor for the scheduler manager, 103 * used only when a new job is scheduled.<p> 104 */ 105 public CmsScheduleManager() { 106 107 // important: this constructor is always called when a new job is 108 // generated, so it _must_ remain empty 109 } 110 111 /** 112 * Used by the configuration to create a new Scheduler during system startup.<p> 113 * 114 * @param configuredJobs the jobs from the configuration 115 */ 116 public CmsScheduleManager(List<CmsScheduledJobInfo> configuredJobs) { 117 118 m_configuredJobs = configuredJobs; 119 int size = 0; 120 if (m_configuredJobs != null) { 121 size = m_configuredJobs.size(); 122 } 123 124 if (CmsLog.INIT.isInfoEnabled()) { 125 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SCHEDULER_CREATED_1, Integer.valueOf(size))); 126 } 127 } 128 129 /** 130 * Implementation of the Quartz job interface.<p> 131 * 132 * The architecture is that this scheduler manager generates 133 * a new (empty) instance of itself for every OpenCms job scheduled with Quartz. 134 * When the Quartz job is executed, the configured 135 * implementation of {@link I_CmsScheduledJob} will be called from this method.<p> 136 * 137 * @see org.quartz.Job#execute(org.quartz.JobExecutionContext) 138 */ 139 public void execute(JobExecutionContext context) { 140 141 JobDataMap jobData = context.getJobDetail().getJobDataMap(); 142 143 CmsScheduledJobInfo jobInfo = (CmsScheduledJobInfo)jobData.get(SCHEDULER_JOB_INFO); 144 145 if (jobInfo == null) { 146 LOG.error( 147 Messages.get().getBundle().key( 148 Messages.LOG_INVALID_JOB_1, 149 ((JobDetailImpl)context.getJobDetail()).getFullName())); 150 // can not continue 151 return; 152 } 153 // update the execution times in job info 154 jobInfo.setPreviousFireTime(context.getFireTime()); 155 jobInfo.setNextFireTime(context.getNextFireTime()); 156 executeJob(jobInfo); 157 } 158 159 /** 160 * Given a job ID, this directly executes the corresponding job.<p> 161 * 162 * @param jobId the job id 163 */ 164 public void executeDirectly(String jobId) { 165 166 final CmsScheduledJobInfo jobInfo = (CmsScheduledJobInfo)getJob(jobId).clone(); 167 if (jobInfo == null) { 168 LOG.error(Messages.get().getBundle().key(Messages.LOG_INVALID_JOB_1, "null")); 169 return; 170 } 171 Thread thread = new Thread() { 172 173 /** 174 * @see java.lang.Thread#run() 175 */ 176 @Override 177 public void run() { 178 179 executeJob(jobInfo); 180 } 181 }; 182 thread.start(); 183 } 184 185 /** 186 * Returns the currently scheduled job description identified by the given id. 187 * 188 * @param id the job id 189 * 190 * @return a job or <code>null</code> if not found 191 */ 192 public CmsScheduledJobInfo getJob(String id) { 193 194 Iterator<CmsScheduledJobInfo> it = m_jobs.iterator(); 195 while (it.hasNext()) { 196 CmsScheduledJobInfo job = it.next(); 197 if (job.getId().equals(id)) { 198 return job; 199 } 200 } 201 // not found 202 return null; 203 } 204 205 /** 206 * Returns the currently scheduled job descriptions in an unmodifiable list.<p> 207 * 208 * The objects in the List are of type <code>{@link CmsScheduledJobInfo}</code>.<p> 209 * 210 * @return the currently scheduled job descriptions in an unmodifiable list 211 */ 212 public List<CmsScheduledJobInfo> getJobs() { 213 214 return Collections.unmodifiableList(m_jobs); 215 } 216 217 /** 218 * Initializes the OpenCms scheduler.<p> 219 * 220 * @param adminCms an OpenCms context object that must have been initialized with "Admin" permissions 221 * 222 * @throws CmsRoleViolationException if the user has insufficient role permissions 223 */ 224 public synchronized void initialize(CmsObject adminCms) throws CmsRoleViolationException { 225 226 if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT) { 227 // simple unit tests will have runlevel 1 and no CmsObject 228 OpenCms.getRoleManager().checkRole(adminCms, CmsRole.WORKPLACE_MANAGER); 229 } 230 231 // the list of job entries 232 m_jobs = new ArrayList<CmsScheduledJobInfo>(); 233 234 // save the admin cms 235 m_adminCms = adminCms; 236 237 // Quartz scheduler settings 238 Properties properties = new Properties(); 239 properties.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, "OpenCmsScheduler"); 240 properties.put(StdSchedulerFactory.PROP_SCHED_THREAD_NAME, "OpenCms: Scheduler"); 241 properties.put(StdSchedulerFactory.PROP_SCHED_RMI_EXPORT, CmsStringUtil.FALSE); 242 properties.put(StdSchedulerFactory.PROP_SCHED_RMI_PROXY, CmsStringUtil.FALSE); 243 properties.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, CmsSchedulerThreadPool.class.getName()); 244 properties.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, "org.quartz.simpl.RAMJobStore"); 245 // this will be required in quartz versions from 1.6, but constants are not supported in earlier versions 246 properties.put("org.quartz.scheduler.jmx.export", CmsStringUtil.FALSE); 247 properties.put("org.quartz.scheduler.jmx.proxy", CmsStringUtil.FALSE); 248 249 try { 250 // initialize the Quartz scheduler 251 SchedulerFactory schedulerFactory = new StdSchedulerFactory(properties); 252 m_scheduler = schedulerFactory.getScheduler(); 253 } catch (Exception e) { 254 LOG.error(Messages.get().getBundle().key(Messages.LOG_NO_SCHEDULER_0), e); 255 // can not continue 256 m_scheduler = null; 257 return; 258 } 259 260 if (CmsLog.INIT.isInfoEnabled()) { 261 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SCHEDULER_INITIALIZED_0)); 262 } 263 264 if (m_configuredJobs != null) { 265 // add all jobs from the system configuration 266 for (int i = 0; i < m_configuredJobs.size(); i++) { 267 try { 268 CmsScheduledJobInfo job = m_configuredJobs.get(i); 269 scheduleJob(adminCms, job); 270 } catch (CmsSchedulerException e) { 271 // ignore this job, but keep scheduling the other jobs 272 // note: the log is has already been written 273 } 274 } 275 } 276 277 try { 278 // start the scheduler 279 m_scheduler.start(); 280 } catch (Exception e) { 281 LOG.error(Messages.get().getBundle().key(Messages.LOG_CANNOT_START_SCHEDULER_0), e); 282 // can not continue 283 m_scheduler = null; 284 return; 285 } 286 287 if (CmsLog.INIT.isInfoEnabled()) { 288 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SCHEDULER_STARTED_0)); 289 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SCHEDULER_CONFIG_FINISHED_0)); 290 } 291 } 292 293 /** 294 * Adds a new job to the scheduler.<p> 295 * 296 * @param cms an OpenCms context object that must have been initialized with "Admin" permissions 297 * @param jobInfo the job info describing the job to schedule 298 * 299 * @throws CmsRoleViolationException if the user has insufficient role permissions 300 * @throws CmsSchedulerException if the job could not be scheduled for any reason 301 */ 302 public synchronized void scheduleJob(CmsObject cms, CmsScheduledJobInfo jobInfo) 303 throws CmsRoleViolationException, CmsSchedulerException { 304 305 if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT) { 306 // simple unit tests will have runlevel 1 and no CmsObject 307 OpenCms.getRoleManager().checkRole(cms, CmsRole.WORKPLACE_MANAGER); 308 } 309 310 if ((jobInfo == null) || (jobInfo.getClassName() == null)) { 311 // prevent NPE 312 CmsMessageContainer message = Messages.get().container(Messages.ERR_INVALID_JOB_CONFIGURATION_0); 313 LOG.error(message.key()); 314 // can not continue 315 throw new CmsSchedulerException(message); 316 } 317 318 if (m_scheduler == null) { 319 CmsMessageContainer message = Messages.get().container(Messages.ERR_NO_SCHEDULER_1, jobInfo.getJobName()); 320 LOG.error(message.key()); 321 // can not continue 322 throw new CmsSchedulerException(message); 323 } 324 325 Class<?> jobClass; 326 try { 327 jobClass = Class.forName(jobInfo.getClassName()); 328 if (!I_CmsScheduledJob.class.isAssignableFrom(jobClass)) { 329 // class does not implement required interface 330 CmsMessageContainer message = Messages.get().container( 331 Messages.ERR_JOB_CLASS_BAD_INTERFACE_2, 332 jobInfo.getClassName(), 333 I_CmsScheduledJob.class.getName()); 334 LOG.error(message.key()); 335 if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_2_INITIALIZING) { 336 throw new CmsIllegalArgumentException(message); 337 } else { 338 jobInfo.setActive(false); 339 } 340 } 341 } catch (ClassNotFoundException e) { 342 // class does not exist 343 CmsMessageContainer message = Messages.get().container( 344 Messages.ERR_JOB_CLASS_NOT_FOUND_1, 345 jobInfo.getClassName()); 346 LOG.error(message.key()); 347 if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_2_INITIALIZING) { 348 throw new CmsIllegalArgumentException(message); 349 } else { 350 jobInfo.setActive(false); 351 } 352 353 } 354 355 String jobId = jobInfo.getId(); 356 boolean idCreated = false; 357 if (jobId == null) { 358 // generate a new job id 359 CmsUUID jobUUID = new CmsUUID(); 360 jobId = "OpenCmsJob_".concat(jobUUID.toString()); 361 jobInfo.setId(jobId); 362 idCreated = true; 363 } 364 365 // generate Quartz job trigger 366 Trigger trigger; 367 try { 368 369 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(jobInfo.getCronExpression()); 370 TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger(); 371 triggerBuilder.withSchedule(scheduleBuilder); 372 triggerBuilder.withIdentity(jobId, Scheduler.DEFAULT_GROUP); 373 trigger = triggerBuilder.build(); 374 375 } catch (Exception e) { 376 if (idCreated) { 377 jobInfo.setId(null); 378 } 379 CmsMessageContainer message = Messages.get().container( 380 Messages.ERR_BAD_CRON_EXPRESSION_2, 381 jobInfo.getJobName(), 382 jobInfo.getCronExpression()); 383 LOG.error(message.key()); 384 // can not continue 385 throw new CmsSchedulerException(message); 386 } 387 388 CmsScheduledJobInfo oldJob = null; 389 if (!idCreated) { 390 // this job is already scheduled, remove the currently scheduled instance and keep the id 391 // important: since the new job may have errors, it's required to make sure the old job is only unscheduled 392 // if the new job info is o.k. 393 oldJob = unscheduleJob(cms, jobId); 394 if (oldJob == null) { 395 CmsMessageContainer message = Messages.get().container( 396 Messages.ERR_JOB_WITH_ID_DOES_NOT_EXIST_1, 397 jobId); 398 LOG.warn(message.key()); 399 // can not continue 400 throw new CmsSchedulerException(message); 401 } 402 // open the job configuration (in case it has been frozen) 403 jobInfo.setFrozen(false); 404 } 405 406 // only schedule jobs when they are marked as active 407 if (jobInfo.isActive()) { 408 409 // generate Quartz job detail 410 JobDetailImpl jobDetail = (JobDetailImpl)JobBuilder.newJob(CmsScheduleManager.class).build(); 411 jobDetail.setName(jobInfo.getId()); 412 jobDetail.setGroup(Scheduler.DEFAULT_GROUP); 413 414 // add the trigger to the job info 415 jobInfo.setTrigger(trigger); 416 417 // now set the job data 418 JobDataMap jobData = new JobDataMap(); 419 jobData.put(CmsScheduleManager.SCHEDULER_JOB_INFO, jobInfo); 420 jobDetail.setJobDataMap(jobData); 421 422 // finally add the job to the Quartz scheduler 423 try { 424 m_scheduler.scheduleJob(jobDetail, trigger); 425 if (LOG.isInfoEnabled()) { 426 LOG.info( 427 Messages.get().getBundle().key( 428 Messages.LOG_JOB_SCHEDULED_4, 429 new Object[] { 430 Integer.valueOf(m_jobs.size()), 431 jobInfo.getJobName(), 432 jobInfo.getClassName(), 433 jobInfo.getContextInfo().getUserName()})); 434 Date nextExecution = jobInfo.getExecutionTimeNext(); 435 if (nextExecution != null) { 436 LOG.info( 437 Messages.get().getBundle().key( 438 Messages.LOG_JOB_NEXT_EXECUTION_2, 439 jobInfo.getJobName(), 440 nextExecution)); 441 } 442 } 443 } catch (Exception e) { 444 if (LOG.isDebugEnabled()) { 445 LOG.debug(e.getMessage(), e); 446 } 447 if (idCreated) { 448 jobInfo.setId(null); 449 } 450 CmsMessageContainer message = Messages.get().container( 451 Messages.ERR_COULD_NOT_SCHEDULE_JOB_2, 452 jobInfo.getJobName(), 453 jobInfo.getClassName()); 454 if (oldJob != null) { 455 // make sure an old job is re-scheduled 456 457 jobDetail = (JobDetailImpl)JobBuilder.newJob(CmsScheduleManager.class).build(); 458 jobDetail.setName(oldJob.getId()); 459 jobDetail.setGroup(Scheduler.DEFAULT_GROUP); 460 jobDetail.setJobDataMap(jobData); 461 try { 462 m_scheduler.scheduleJob(jobDetail, oldJob.getTrigger()); 463 m_jobs.add(oldJob); 464 } catch (SchedulerException e2) { 465 if (LOG.isDebugEnabled()) { 466 LOG.debug(e2.getMessage(), e2); 467 } 468 // unable to re-schedule original job - not much we can do about this... 469 message = Messages.get().container( 470 Messages.ERR_COULD_NOT_RESCHEDULE_JOB_2, 471 jobInfo.getJobName(), 472 jobInfo.getClassName()); 473 } 474 } 475 if (LOG.isWarnEnabled()) { 476 LOG.warn(message.key()); 477 } 478 throw new CmsSchedulerException(message); 479 } 480 } 481 482 // freeze the scheduled job configuration 483 jobInfo.initConfiguration(); 484 485 // add the job to the list of configured jobs 486 m_jobs.add(jobInfo); 487 488 } 489 490 /** 491 * Shuts down this instance of the OpenCms scheduler manager.<p> 492 */ 493 public synchronized void shutDown() { 494 495 m_adminCms = null; 496 497 if (CmsLog.INIT.isInfoEnabled()) { 498 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_1, this.getClass().getName())); 499 } 500 501 if (m_scheduler != null) { 502 try { 503 m_scheduler.shutdown(); 504 } catch (SchedulerException e) { 505 LOG.error(Messages.get().getBundle().key(Messages.LOG_SHUTDOWN_ERROR_0)); 506 } 507 } 508 509 m_scheduler = null; 510 } 511 512 /** 513 * Removes a currently scheduled job from the scheduler.<p> 514 * 515 * @param cms an OpenCms context object that must have been initialized with "Admin" permissions 516 * @param jobId the id of the job to unschedule, obtained with <code>{@link CmsScheduledJobInfo#getId()}</code> 517 * 518 * @return the <code>{@link CmsScheduledJobInfo}</code> of the sucessfully unscheduled job, 519 * or <code>null</code> if the job could not be unscheduled 520 * 521 * @throws CmsRoleViolationException if the user has insufficient role permissions 522 */ 523 public synchronized CmsScheduledJobInfo unscheduleJob(CmsObject cms, String jobId) 524 throws CmsRoleViolationException { 525 526 if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT) { 527 // simple unit tests will have runlevel 1 and no CmsObject 528 OpenCms.getRoleManager().checkRole(cms, CmsRole.WORKPLACE_MANAGER); 529 } 530 531 CmsScheduledJobInfo jobInfo = null; 532 if (m_jobs.size() > 0) { 533 // try to remove the job from the OpenCms list of jobs 534 for (int i = (m_jobs.size() - 1); i >= 0; i--) { 535 CmsScheduledJobInfo job = m_jobs.get(i); 536 if (jobId.equals(job.getId())) { 537 m_jobs.remove(i); 538 if (jobInfo != null) { 539 LOG.error(Messages.get().getBundle().key(Messages.LOG_MULTIPLE_JOBS_FOUND_1, jobId)); 540 } 541 jobInfo = job; 542 } 543 } 544 } 545 546 if ((jobInfo != null) && jobInfo.isActive()) { 547 // job currently active, remove it from the Quartz scheduler 548 try { 549 // try to remove the job from Quartz 550 m_scheduler.unscheduleJob(new TriggerKey(jobId, Scheduler.DEFAULT_GROUP)); 551 if (LOG.isDebugEnabled()) { 552 LOG.debug(Messages.get().getBundle().key(Messages.LOG_UNSCHEDULED_JOB_1, jobId)); 553 } 554 } catch (SchedulerException e) { 555 if (LOG.isDebugEnabled()) { 556 LOG.debug(Messages.get().getBundle().key(Messages.LOG_UNSCHEDULING_ERROR_1, jobId)); 557 } 558 } 559 } 560 561 return jobInfo; 562 } 563 564 /** 565 * Executes the given job.<p> 566 * 567 * @param jobInfo the job info bean 568 */ 569 protected void executeJob(CmsScheduledJobInfo jobInfo) { 570 571 if (LOG.isDebugEnabled()) { 572 LOG.debug(Messages.get().getBundle().key(Messages.LOG_JOB_STARTING_1, jobInfo.getJobName())); 573 } 574 575 I_CmsScheduledJob job = jobInfo.getJobInstance(); 576 577 if (job != null) { 578 // launch the job 579 try { 580 581 CmsObject cms = null; 582 // update the request time in the job info to the current time 583 jobInfo.updateContextRequestTime(); 584 // some simple test cases might run below this runlevel 585 if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) { 586 // generate a CmsObject for the job context 587 // must access the scheduler manager instance from the OpenCms singleton 588 // to get the initialized CmsObject 589 cms = OpenCms.initCmsObject(OpenCms.getScheduleManager().getAdminCms(), jobInfo.getContextInfo()); 590 } 591 592 String result = job.launch(cms, jobInfo.getParameters()); 593 if (CmsStringUtil.isNotEmpty(result) && LOG.isInfoEnabled()) { 594 LOG.info( 595 Messages.get().getBundle().key(Messages.LOG_JOB_EXECUTION_OK_2, jobInfo.getJobName(), result)); 596 } 597 } catch (Throwable t) { 598 LOG.error(Messages.get().getBundle().key(Messages.LOG_JOB_EXECUTION_ERROR_1, jobInfo.getJobName()), t); 599 } 600 } 601 602 if (LOG.isDebugEnabled()) { 603 LOG.debug(Messages.get().getBundle().key(Messages.LOG_JOB_EXECUTED_1, jobInfo.getJobName())); 604 Date nextExecution = jobInfo.getExecutionTimeNext(); 605 if (nextExecution != null) { 606 LOG.info( 607 Messages.get().getBundle().key( 608 Messages.LOG_JOB_NEXT_EXECUTION_2, 609 jobInfo.getJobName(), 610 nextExecution)); 611 } 612 } 613 } 614 615 /** 616 * Returns the {@link CmsObject} this Scheduler Manager was initialized with.<p> 617 * 618 * @return the {@link CmsObject} this Scheduler Manager was initialized with 619 */ 620 private synchronized CmsObject getAdminCms() { 621 622 return m_adminCms; 623 } 624}