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.
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     * @exception 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     * @exception 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