docs: public synchronized void scheduleJob(CmsObject cms, CmsScheduledJobInfo jobInfo)
      throws CmsRoleViolationException, CmsSchedulerException {
  
          if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT) {
              // simple unit tests will have runlevel 1 and no CmsObject
              OpenCms.getRoleManager().checkRole(cms, CmsRole.WORKPLACE_MANAGER);
          }
  
          if ((jobInfo == null) || (jobInfo.getClassName() == null)) {
              // prevent NPE
              CmsMessageContainer message = Messages.get().container(Messages.ERR_INVALID_JOB_CONFIGURATION_0);
              LOG.error(message.key());
              // can not continue
              throw new CmsSchedulerException(message);
          }
  
          if (m_scheduler == null) {
              CmsMessageContainer message = Messages.get().container(Messages.ERR_NO_SCHEDULER_1, jobInfo.getJobName());
              LOG.error(message.key());
              // can not continue
              throw new CmsSchedulerException(message);
          }
  
          Class<?> jobClass;
          try {
              jobClass = Class.forName(jobInfo.getClassName());
              if (!I_CmsScheduledJob.class.isAssignableFrom(jobClass)) {
                  // class does not implement required interface
                  CmsMessageContainer message = Messages.get().container(
                      Messages.ERR_JOB_CLASS_BAD_INTERFACE_2,
                      jobInfo.getClassName(),
                      I_CmsScheduledJob.class.getName());
                  LOG.error(message.key());
                  if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_2_INITIALIZING) {
                      throw new CmsIllegalArgumentException(message);
                  } else {
                      jobInfo.setActive(false);
                  }
              }
          } catch (ClassNotFoundException e) {
              // class does not exist
              CmsMessageContainer message = Messages.get().container(
                  Messages.ERR_JOB_CLASS_NOT_FOUND_1,
                  jobInfo.getClassName());
              LOG.error(message.key());
              if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_2_INITIALIZING) {
                  throw new CmsIllegalArgumentException(message);
              } else {
                  jobInfo.setActive(false);
              }
  
          }
  
          String jobId = jobInfo.getId();
          boolean idCreated = false;
          if (jobId == null) {
              // generate a new job id
              CmsUUID jobUUID = new CmsUUID();
              jobId = "OpenCmsJob_".concat(jobUUID.toString());
              jobInfo.setId(jobId);
              idCreated = true;
          }
  
          // generate Quartz job trigger
          Trigger trigger;
          try {
  
              CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(jobInfo.getCronExpression());
              TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
              triggerBuilder.withSchedule(scheduleBuilder);
              triggerBuilder.withIdentity(jobId, Scheduler.DEFAULT_GROUP);
              trigger = triggerBuilder.build();
  
          } catch (Exception e) {
              if (idCreated) {
                  jobInfo.setId(null);
              }
              CmsMessageContainer message = Messages.get().container(
                  Messages.ERR_BAD_CRON_EXPRESSION_2,
                  jobInfo.getJobName(),
                  jobInfo.getCronExpression());
              LOG.error(message.key());
              // can not continue
              throw new CmsSchedulerException(message);
          }
  
          CmsScheduledJobInfo oldJob = null;
          if (!idCreated) {
              // this job is already scheduled, remove the currently scheduled instance and keep the id
              // important: since the new job may have errors, it's required to make sure the old job is only unscheduled
              // if the new job info is o.k.
              oldJob = unscheduleJob(cms, jobId);
              if (oldJob == null) {
                  CmsMessageContainer message = Messages.get().container(
                      Messages.ERR_JOB_WITH_ID_DOES_NOT_EXIST_1,
                      jobId);
                  LOG.warn(message.key());
                  // can not continue
                  throw new CmsSchedulerException(message);
              }
              // open the job configuration (in case it has been frozen)
              jobInfo.setFrozen(false);
          }
  
          // only schedule jobs when they are marked as active
          if (jobInfo.isActive()) {
  
              // generate Quartz job detail
              JobDetailImpl jobDetail = (JobDetailImpl)JobBuilder.newJob(CmsScheduleManager.class).build();
              jobDetail.setName(jobInfo.getId());
              jobDetail.setGroup(Scheduler.DEFAULT_GROUP);
  
              // add the trigger to the job info
              jobInfo.setTrigger(trigger);
  
              // now set the job data
              JobDataMap jobData = new JobDataMap();
              jobData.put(CmsScheduleManager.SCHEDULER_JOB_INFO, jobInfo);
              jobDetail.setJobDataMap(jobData);
  
              // finally add the job to the Quartz scheduler
              try {
                  m_scheduler.scheduleJob(jobDetail, trigger);
                  if (LOG.isInfoEnabled()) {
                      LOG.info(
                          Messages.get().getBundle().key(
                              Messages.LOG_JOB_SCHEDULED_4,
                              new Object[] {
                                  new Integer(m_jobs.size()),
                                  jobInfo.getJobName(),
                                  jobInfo.getClassName(),
                                  jobInfo.getContextInfo().getUserName()}));
                      Date nextExecution = jobInfo.getExecutionTimeNext();
                      if (nextExecution != null) {
                          LOG.info(
                              Messages.get().getBundle().key(
                                  Messages.LOG_JOB_NEXT_EXECUTION_2,
                                  jobInfo.getJobName(),
                                  nextExecution));
                      }
                  }
              } catch (Exception e) {
                  if (LOG.isDebugEnabled()) {
                      LOG.debug(e.getMessage(), e);
                  }
                  if (idCreated) {
                      jobInfo.setId(null);
                  }
                  CmsMessageContainer message = Messages.get().container(
                      Messages.ERR_COULD_NOT_SCHEDULE_JOB_2,
                      jobInfo.getJobName(),
                      jobInfo.getClassName());
                  if (oldJob != null) {
                      // make sure an old job is re-scheduled
  
                      jobDetail = (JobDetailImpl)JobBuilder.newJob(CmsScheduleManager.class).build();
                      jobDetail.setName(oldJob.getId());
                      jobDetail.setGroup(Scheduler.DEFAULT_GROUP);
                      jobDetail.setJobDataMap(jobData);
                      try {
                          m_scheduler.scheduleJob(jobDetail, oldJob.getTrigger());
                          m_jobs.add(oldJob);
                      } catch (SchedulerException e2) {
                          if (LOG.isDebugEnabled()) {
                              LOG.debug(e2.getMessage(), e2);
                          }
                          // unable to re-schedule original job - not much we can do about this...
                          message = Messages.get().container(
                              Messages.ERR_COULD_NOT_RESCHEDULE_JOB_2,
                              jobInfo.getJobName(),
                              jobInfo.getClassName());
                      }
                  }
                  if (LOG.isWarnEnabled()) {
                      LOG.warn(message.key());
                  }
                  throw new CmsSchedulerException(message);
              }
          }
  
          // freeze the scheduled job configuration
          jobInfo.initConfiguration();
  
          // add the job to the list of configured jobs
          m_jobs.add(jobInfo);
  
      }