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.fulcrum.quartz.QuartzScheduler;
028import org.apache.logging.log4j.LogManager;
029import org.apache.logging.log4j.Logger;
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 final Logger log = LogManager.getLogger(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<>();
264
265        try
266        {
267            GroupMatcher<JobKey> groupMatcher = GroupMatcher.groupEquals(JobEntryQuartz.DEFAULT_JOB_GROUP_NAME);
268            Set<JobKey> jobKeys = scheduler.getJobKeys(groupMatcher);
269            for (JobKey jk : jobKeys)
270            {
271                List<? extends Trigger> triggers = this.scheduler.getTriggersOfJob(jk);
272
273                if (triggers == null || triggers.isEmpty())
274                {
275                    continue; // skip
276                }
277                JobDetail jd = this.scheduler.getJobDetail(jk);
278                JobEntryQuartz job = new JobEntryQuartz(triggers.get(0), jd);
279                job.setJobId(jk.hashCode());
280                jobs.add(job);
281            }
282        }
283        catch (SchedulerException e)
284        {
285            log.error("Problem listing Scheduled Jobs", e);
286        }
287
288        return jobs;
289    }
290
291
292    /**
293     * Sets the enabled status of the scheduler
294     *
295     * @param enabled true if enabled
296     *
297     */
298    protected void setEnabled(boolean enabled)
299    {
300        this.enabled = enabled;
301    }
302
303    /**
304     * Determines if the scheduler service is currently enabled.
305     *
306     * @return Status of the scheduler service.
307     */
308    @Override
309    public boolean isEnabled()
310    {
311        return enabled;
312    }
313
314    /**
315     * Starts or restarts the scheduler if not already running.
316     */
317    @Override
318    public synchronized void startScheduler()
319    {
320        setEnabled(true);
321        restart();
322    }
323
324    /**
325     * Stops the scheduler if it is currently running.
326     */
327    @Override
328    public synchronized void stopScheduler()
329    {
330        log.info("Stopping job scheduler");
331        try
332        {
333            this.scheduler.standby();
334            enabled = false;
335        }
336        catch (SchedulerException e)
337        {
338            log.error("Could not stop scheduler", e);
339        }
340    }
341
342    /**
343     * Start (or restart) a thread to process commands, or wake up an
344     * existing thread if one is already running.  This method can be
345     * invoked if the background thread crashed due to an
346     * unrecoverable exception in an executed command.
347     */
348    public synchronized void restart()
349    {
350        if (enabled)
351        {
352            log.info("Starting job scheduler");
353            try
354            {
355                if (!this.scheduler.isStarted())
356                {
357                    this.scheduler.start();
358                }
359                else
360                {
361                    notify();
362                }
363            }
364            catch (SchedulerException e)
365            {
366                log.error("Could not start scheduler", e);
367            }
368        }
369    }
370
371    /**
372     * @param je a generic job entry
373     * @throws TurbineException
374     *
375     * @return A downcasted JobEntry type
376     */
377    private JobEntryQuartz downCast(JobEntry je) throws TurbineException
378    {
379        if (je instanceof JobEntryQuartz)
380        {
381            return (JobEntryQuartz)je;
382        }
383        else
384        {
385            throw new TurbineException("Invalid job type for this scheduler " + je.getClass());
386        }
387    }
388
389    /**
390     * Exposing the Quartz scheduler to handle jobs/triggers in more detail.
391     *
392     * @return the {@link Scheduler} of this service.
393     */
394        public Scheduler getScheduler()
395        {
396                return scheduler;
397        }
398
399        /**
400         * Builds a {@link JobEntryQuartz} from Quartz trigger/job.
401         *
402         * The developer should be aware to set identity/context properly, i.e. to
403     * {@link JobEntryQuartz#DEFAULT_JOB_GROUP_NAME}, if adding triggers/jobs.
404         *
405         * @param trigger a Quartz {@link Trigger}.
406         * @param jd a Quartz {@link JobDetail} (built from a {@link org.quartz.Job} with {@link JobBuilder}).
407         * @return A JobEntryQuartz.
408         */
409        public JobEntryQuartz buildJobEntry(Trigger trigger, JobDetail jd) {
410        JobEntryQuartz job = new JobEntryQuartz(trigger, jd);
411                return job;
412        }
413}
414