    001package org.apache.turbine.services.schedule;
    002
    003/*
    004 * Licensed to the Apache Software Foundation (ASF) under one
    005 * or more contributor license agreements.  See the NOTICE file
    006 * distributed with this work for additional information
    007 * regarding copyright ownership.  The ASF licenses this file
    008 * to you under the Apache License, Version 2.0 (the
    009 * "License"); you may not use this file except in compliance
    010 * with the License.  You may obtain a copy of the License at
    011 *
    012 *   http://www.apache.org/licenses/LICENSE-2.0
    013 *
    014 * Unless required by applicable law or agreed to in writing,
    015 * software distributed under the License is distributed on an
    016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    017 * KIND, either express or implied.  See the License for the
    018 * specific language governing permissions and limitations
    019 * under the License.
    020 */
    021
    022import java.text.ParseException;
    023import java.util.ArrayList;
    024import java.util.List;
    025import java.util.Set;
    026
    027import org.apache.commons.logging.Log;
    028import org.apache.commons.logging.LogFactory;
    029import org.apache.fulcrum.quartz.QuartzScheduler;
    030import org.apache.turbine.services.InitializationException;
    031import org.apache.turbine.services.TurbineBaseService;
    032import org.apache.turbine.services.TurbineServices;
    033import org.apache.turbine.util.TurbineException;
    034import org.quartz.CronScheduleBuilder;
    035import org.quartz.JobBuilder;
    036import org.quartz.JobDetail;
    037import org.quartz.JobKey;
    038import org.quartz.Scheduler;
    039import org.quartz.SchedulerException;
    040import org.quartz.Trigger;
    041import org.quartz.TriggerBuilder;
    042import org.quartz.impl.matchers.GroupMatcher;
    043
    044/**
    045 * Service for a quartz scheduler.
    046 *
    047 * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
    048 */
    049public class QuartzSchedulerService
    050        extends TurbineBaseService
    051        implements ScheduleService
    052{
    053    /** Logging */
    054    protected static Log log = LogFactory.getLog(ScheduleService.LOGGER_NAME);
    055
    056    /** Current status of the scheduler */
    057    protected boolean enabled = false;
    058
    059    /** The Quartz scheduler instance */
    060    private Scheduler scheduler;
    061
    062    /**
    063     * Initializes the SchedulerService. Retrieves the Quartz {@link #scheduler} from the Fulcrum {@link QuartzScheduler} service.
    064     *
    065     * @throws InitializationException Something went wrong in the init
    066     *         stage
    067     */
    068    @Override
    069    public void init()
    070            throws InitializationException
    071    {
    072        setEnabled(getConfiguration().getBoolean("enabled", true));
    073        QuartzScheduler qs = (QuartzScheduler) TurbineServices.getInstance()
    074            .getService(QuartzScheduler.class.getName());
    075        this.scheduler = qs.getScheduler();
    076
    077        restart();
    078        setInit(true);
    079    }
    080
    081    /**
    082     * Shutdowns the service.
    083     *
    084     * This methods interrupts the housekeeping thread.
    085     */
    086    @Override
    087    public void shutdown()
    088    {
    089        try
    090        {
    091            this.scheduler.shutdown();
    092        }
    093        catch (SchedulerException e)
    094        {
    095            log.error("Could not shut down the scheduler service", e);
    096        }
    097    }
    098
    099    /**
    100     * @see org.apache.turbine.services.schedule.ScheduleService#newJob(int, int, int, int, int, java.lang.String)
    101     */
    102    @Override
    103    public JobEntry newJob(int sec, int min, int hour, int wd, int day_mo, String task) throws TurbineException
    104    {
    105        try
    106        {
    107            JobDetail jd = JobBuilder.newJob(JobEntryQuartz.class)
    108                    .withIdentity(task, JobEntryQuartz.DEFAULT_JOB_GROUP_NAME)
    109                    .build();
    110
    111            CronScheduleBuilder csb = createCronExpression(sec, min, hour, wd, day_mo);
    112
    113            Trigger t = TriggerBuilder.newTrigger()
    114                    .withIdentity(task, JobEntryQuartz.DEFAULT_JOB_GROUP_NAME)
    115                    .withSchedule(csb)
    116                    .forJob(jd)
    117                    .build();
    118
    119            JobEntryQuartz jeq = new JobEntryQuartz(t, jd);
    120
    121            return jeq;
    122        }
    123        catch (ParseException e)
    124        {
    125            throw new TurbineException("Could not create scheduled job " + task, e);
    126        }
    127    }
    128
    129    /**
    130     * Create a Cron expression from separate elements
    131     *
    132     * @param sec Value for entry "seconds".
    133     * @param min Value for entry "minutes".
    134     * @param hour Value for entry "hours".
    135     * @param wd Value for entry "week days".
    136     * @param day_mo Value for entry "month days".
    137     * @return a CronScheduleBuilder
    138     * @throws ParseException if the expression is invalid
    139     */
    140    private CronScheduleBuilder createCronExpression(int sec, int min, int hour, int wd, int day_mo) throws ParseException
    141    {
    142        StringBuilder sb = new StringBuilder();
    143        sb.append(sec == -1 ? "*" : String.valueOf(sec)).append(' ');
    144        sb.append(min == -1 ? "*" : String.valueOf(min)).append(' ');
    145        sb.append(hour == -1 ? "*" : String.valueOf(hour)).append(' ');
    146        if (day_mo == -1)
    147        {
    148            sb.append(wd == -1 ? "*" : "?").append(' ');
    149        }
    150        else
    151        {
    152            sb.append(day_mo).append(' ');
    153        }
    154        sb.append("* "); // Month not supported
    155        if (day_mo == -1)
    156        {
    157            sb.append(wd == -1 ? "?" : String.valueOf(wd));
    158        }
    159        else
    160        {
    161            sb.append("*");
    162        }
    163
    164        return CronScheduleBuilder.cronSchedule(sb.toString());
    165    }
    166
    167    /**
    168     * Get a specific Job from Storage.
    169     *
    170     * @param oid The int id for the job.
    171     * @return A JobEntry.
    172     * @throws TurbineException job could not be retrieved.
    173     */
    174    @Override
    175    public JobEntry getJob(int oid)
    176            throws TurbineException
    177    {
    178        for (JobEntry je : listJobs())
    179        {
    180            if (je.getJobId() == oid)
    181            {
    182                return je;
    183            }
    184        }
    185
    186        throw new TurbineException("Could not retrieve scheduled job with id " + oid);
    187    }
    188
    189    /**
    190     * Add a new job to the queue.
    191     *
    192     * @param je A JobEntry with the job to add.
    193     * @throws TurbineException job could not be added
    194     */
    195    @Override
    196    public void addJob(JobEntry je)
    197            throws TurbineException
    198    {
    199        try
    200        {
    201            // Update the scheduler.
    202            JobEntryQuartz jq = downCast(je);
    203            this.scheduler.scheduleJob(jq.getJobDetail(), jq.getJobTrigger());
    204        }
    205        catch (SchedulerException e)
    206        {
    207            throw new TurbineException("Problem adding Scheduled Job: " + je.getTask(), e);
    208        }
    209    }
    210
    211    /**
    212     * Remove a job from the queue.
    213     *
    214     * @param je A JobEntry with the job to remove.
    215     * @throws TurbineException job could not be removed
    216     */
    217    @Override
    218    public void removeJob(JobEntry je)
    219            throws TurbineException
    220    {
    221        try
    222        {
    223            JobEntryQuartz jq = downCast(je);
    224            this.scheduler.deleteJob(jq.getJobTrigger().getJobKey());
    225
    226        }
    227        catch (SchedulerException e)
    228        {
    229            throw new TurbineException("Problem removing Scheduled Job: " + je.getTask(), e);
    230        }
    231    }
    232
    233    /**
    234     * Add or update a job.
    235     *
    236     * @param je A JobEntry with the job to modify
    237     * @throws TurbineException job could not be updated
    238     */
    239    @Override
    240    public void updateJob(JobEntry je)
    241            throws TurbineException
    242    {
    243        try
    244        {
    245            // Update the scheduler.
    246            JobEntryQuartz jq = downCast(je);
    247            this.scheduler.rescheduleJob(jq.getJobTrigger().getKey(), jq.getJobTrigger());
    248        }
    249        catch (SchedulerException e)
    250        {
    251            throw new TurbineException("Problem updating Scheduled Job: " + je.getTask(), e);
    252        }
    253    }
    254
    255    /**
    256     * List jobs in the queue.  This is used by the scheduler UI.
    257     *
    258     * @return A List of jobs.
    259     */
    260    @Override
    261    public List<? extends JobEntry> listJobs()
    262    {
    263        List<JobEntryQuartz> jobs = new ArrayList<JobEntryQuartz>();
    264
    265        try
    266        {
    267            @SuppressWarnings("unchecked") // See QTZ-184
    268            GroupMatcher<JobKey> groupMatcher = GroupMatcher.groupEquals(JobEntryQuartz.DEFAULT_JOB_GROUP_NAME);
    269            Set<JobKey> jobKeys = scheduler.getJobKeys(groupMatcher);
    270            for (JobKey jk : jobKeys)
    271            {
    272                List<? extends Trigger> triggers = this.scheduler.getTriggersOfJob(jk);
    273
    274                if (triggers == null || triggers.isEmpty())
    275                {
    276                    continue; // skip
    277                }
    278                JobDetail jd = this.scheduler.getJobDetail(jk);
    279                JobEntryQuartz job = new JobEntryQuartz(triggers.get(0), jd);
    280                job.setJobId(jk.hashCode());
    281                jobs.add(job);
    282            }
    283        }
    284        catch (SchedulerException e)
    285        {
    286            log.error("Problem listing Scheduled Jobs", e);
    287        }
    288
    289        return jobs;
    290    }
    291
    292
    293    /**
    294     * Sets the enabled status of the scheduler
    295     *
    296     * @param enabled
    297     *
    298     */
    299    protected void setEnabled(boolean enabled)
    300    {
    301        this.enabled = enabled;
    302    }
    303
    304    /**
    305     * Determines if the scheduler service is currently enabled.
    306     *
    307     * @return Status of the scheduler service.
    308     */
    309    @Override
    310    public boolean isEnabled()
    311    {
    312        return enabled;
    313    }
    314
    315    /**
    316     * Starts or restarts the scheduler if not already running.
    317     */
    318    @Override
    319    public synchronized void startScheduler()
    320    {
    321        setEnabled(true);
    322        restart();
    323    }
    324
    325    /**
    326     * Stops the scheduler if it is currently running.
    327     */
    328    @Override
    329    public synchronized void stopScheduler()
    330    {
    331        log.info("Stopping job scheduler");
    332        try
    333        {
    334            this.scheduler.standby();
    335            enabled = false;
    336        }
    337        catch (SchedulerException e)
    338        {
    339            log.error("Could not stop scheduler", e);
    340        }
    341    }
    342
    343    /**
    344     * Start (or restart) a thread to process commands, or wake up an
    345     * existing thread if one is already running.  This method can be
    346     * invoked if the background thread crashed due to an
    347     * unrecoverable exception in an executed command.
    348     */
    349    public synchronized void restart()
    350    {
    351        if (enabled)
    352        {
    353            log.info("Starting job scheduler");
    354            try
    355            {
    356                if (!this.scheduler.isStarted())
    357                {
    358                    this.scheduler.start();
    359                }
    360                else
    361                {
    362                    notify();
    363                }
    364            }
    365            catch (SchedulerException e)
    366            {
    367                log.error("Could not start scheduler", e);
    368            }
    369        }
    370    }
    371
    372    /**
    373     * @param je a generic job entry
    374     * @throws TurbineException
    375     *
    376     * @return A downcasted JobEntry type
    377     */
    378    private JobEntryQuartz downCast(JobEntry je) throws TurbineException
    379    {
    380        if (je instanceof JobEntryQuartz)
    381        {
    382            return (JobEntryQuartz)je;
    383        }
    384        else
    385        {
    386            throw new TurbineException("Invalid job type for this scheduler " + je.getClass());
    387        }
    388    }
    389
    390    /**
    391     * Exposing the Quartz scheduler to handle jobs/triggers in more detail.
    392     *
    393     * @return the {@link Scheduler} of this service.
    394     */
    395        public Scheduler getScheduler()
    396        {
    397                return scheduler;
    398        }
    399
    400        /**
    401         * Builds a {@link JobEntryQuartz} from Quartz trigger/job.
    402         *
    403         * The developer should be aware to set identity/context properly, i.e. to
    404     * {@value JobEntryQuartz#DEFAULT_JOB_GROUP_NAME}, if adding triggers/jobs.
    405         *
    406         * @param trigger a Quartz {@link Trigger}.
    407         * @param jd a Quartz {@link JobDetail} (built from a {@link Job} with {@link JobBuilder}).
    408         * @return A JobEntryQuartz.
    409         */
    410        public JobEntryQuartz buildJobEntry(Trigger trigger, JobDetail jd) {
    411        JobEntryQuartz job = new JobEntryQuartz(trigger, jd);
    412                return job;
    413        }
    414}
    415