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}