1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package com.xpn.xwiki.plugin.scheduler

File SchedulerPlugin.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart9.png
38% of files have more coverage

Code metrics

34
218
28
1
690
454
74
0.34
7.79
28
2.64

Classes

Class Line # Actions
SchedulerPlugin 76 218 0% 74 53
0.810714381.1%
 

Contributing tests

No tests hitting this source file were found.

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package com.xpn.xwiki.plugin.scheduler;
21   
22    import java.net.URL;
23    import java.util.ArrayList;
24    import java.util.Arrays;
25    import java.util.Date;
26    import java.util.List;
27   
28    import javax.inject.Provider;
29   
30    import org.apache.commons.lang3.exception.ExceptionUtils;
31    import org.quartz.CronScheduleBuilder;
32    import org.quartz.Job;
33    import org.quartz.JobBuilder;
34    import org.quartz.JobDataMap;
35    import org.quartz.JobKey;
36    import org.quartz.Scheduler;
37    import org.quartz.SchedulerException;
38    import org.quartz.Trigger;
39    import org.quartz.Trigger.TriggerState;
40    import org.quartz.TriggerBuilder;
41    import org.quartz.TriggerKey;
42    import org.quartz.impl.StdSchedulerFactory;
43    import org.slf4j.Logger;
44    import org.slf4j.LoggerFactory;
45    import org.xwiki.bridge.event.DocumentCreatedEvent;
46    import org.xwiki.bridge.event.DocumentDeletedEvent;
47    import org.xwiki.bridge.event.DocumentUpdatedEvent;
48    import org.xwiki.context.concurrent.ExecutionContextRunnable;
49    import org.xwiki.model.reference.DocumentReference;
50    import org.xwiki.model.reference.EntityReference;
51    import org.xwiki.observation.EventListener;
52    import org.xwiki.observation.ObservationManager;
53    import org.xwiki.observation.event.Event;
54    import org.xwiki.script.service.ScriptServiceManager;
55   
56    import com.xpn.xwiki.XWikiContext;
57    import com.xpn.xwiki.XWikiException;
58    import com.xpn.xwiki.api.Api;
59    import com.xpn.xwiki.doc.XWikiDocument;
60    import com.xpn.xwiki.objects.BaseObject;
61    import com.xpn.xwiki.plugin.XWikiDefaultPlugin;
62    import com.xpn.xwiki.plugin.XWikiPluginInterface;
63    import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobClassDocumentInitializer;
64    import com.xpn.xwiki.plugin.scheduler.internal.StatusListener;
65    import com.xpn.xwiki.web.Utils;
66    import com.xpn.xwiki.web.XWikiResponse;
67    import com.xpn.xwiki.web.XWikiServletRequest;
68    import com.xpn.xwiki.web.XWikiServletRequestStub;
69    import com.xpn.xwiki.web.XWikiServletResponseStub;
70   
71    /**
72    * See {@link com.xpn.xwiki.plugin.scheduler.SchedulerPluginApi} for documentation.
73    *
74    * @version $Id: abd58368fa746670c79d8979f0d782a2a352581e $
75    */
 
76    public class SchedulerPlugin extends XWikiDefaultPlugin implements EventListener
77    {
78    /**
79    * Log object to log messages in this class.
80    */
81    private static final Logger LOGGER = LoggerFactory.getLogger(SchedulerPlugin.class);
82   
83    /**
84    * Fullname of the XWiki Scheduler Job Class representing a job that can be scheduled by this plugin.
85    *
86    * @deprecated use {@link #XWIKI_JOB_CLASSREFERENCE} instead
87    */
88    @Deprecated
89    public static final String XWIKI_JOB_CLASS = "XWiki.SchedulerJobClass";
90   
91    /**
92    * Local reference of the XWiki Scheduler Job Class representing a job that can be scheduled by this plugin.
93    */
94    public static final EntityReference XWIKI_JOB_CLASSREFERENCE =
95    SchedulerJobClassDocumentInitializer.XWIKI_JOB_CLASSREFERENCE;
96   
97    private static final List<Event> EVENTS = Arrays.<Event>asList(new DocumentCreatedEvent(),
98    new DocumentDeletedEvent(), new DocumentUpdatedEvent());
99   
100    /**
101    * Default Quartz scheduler instance.
102    */
103    private Scheduler scheduler;
104   
105    /**
106    * Default plugin constructor.
107    *
108    * @see XWikiDefaultPlugin#XWikiDefaultPlugin(String,String,com.xpn.xwiki.XWikiContext)
109    */
 
110  2 toggle public SchedulerPlugin(String name, String className, XWikiContext context)
111    {
112  2 super(name, className, context);
113    }
114   
 
115  2 toggle @Override
116    public void init(XWikiContext context)
117    {
118  2 Thread thread = new Thread(new ExecutionContextRunnable(new Runnable()
119    {
 
120  2 toggle @Override
121    public void run()
122    {
123  2 initAsync();
124    }
125    }, Utils.getComponentManager()));
126  2 thread.setName("XWiki Scheduler initialization");
127  2 thread.setDaemon(true);
128   
129  2 thread.start();
130   
131    // Start listening to documents modifications
132  2 Utils.getComponent(ObservationManager.class).addListener(this);
133    }
134   
 
135  2 toggle private void initAsync()
136    {
137  2 XWikiContext xcontext = Utils.<Provider<XWikiContext>>getComponent(XWikiContext.TYPE_PROVIDER).get();
138   
139  2 try {
140  2 String initialDb = !xcontext.getWikiId().equals("") ? xcontext.getWikiId() : xcontext.getMainXWiki();
141   
142  2 List<String> wikiServers = new ArrayList<String>();
143  2 try {
144  2 wikiServers = xcontext.getWiki().getVirtualWikisDatabaseNames(xcontext);
145    } catch (Exception e) {
146  0 LOGGER.error("error getting list of wiki servers!", e);
147    }
148   
149    // Before we start the thread ensure that Quartz will create daemon threads so that
150    // the JVM can exit properly.
151  2 System.setProperty("org.quartz.scheduler.makeSchedulerThreadDaemon", "true");
152  2 System.setProperty("org.quartz.threadPool.makeThreadsDaemons", "true");
153   
154  2 setScheduler(getDefaultSchedulerInstance());
155  2 setStatusListener();
156  2 getScheduler().start();
157   
158    // Restore jobs
159   
160  2 try {
161    // Iterate on all virtual wikis
162  2 for (String wikiName : wikiServers) {
163  2 xcontext.setWikiId(wikiName);
164  2 restoreExistingJobs(xcontext);
165    }
166    } finally {
167  2 xcontext.setWikiId(initialDb);
168    }
169    } catch (SchedulerException e) {
170  0 LOGGER.error("Failed to start the scheduler", e);
171    } catch (SchedulerPluginException e) {
172  0 LOGGER.error("Failed to initialize the scheduler", e);
173    }
174    }
175   
176    /**
177    * Create and feed a stub context for the job execution thread. Stub context data are retrieved from job object
178    * fields "contextUser", "contextLang", "contextDatabase". If one of this field is empty (this would typically
179    * happen on the first schedule operation), it is instead retrieved from the passed context, and the job object is
180    * updated with this value. This mean that this method may modify the passed object.
181    *
182    * @param job the job for which the context will be prepared
183    * @param context the XWikiContext at preparation time. This is a real context associated with a servlet request
184    * @return the stub context prepared with job data
185    */
 
186  4 toggle private XWikiContext prepareJobStubContext(BaseObject job, XWikiContext context) throws SchedulerPluginException
187    {
188  4 boolean jobNeedsUpdate = false;
189  4 String cUser = job.getStringValue("contextUser");
190  4 if (cUser.equals("")) {
191    // The context user has not been filled yet.
192    // We can suppose it's the first scheduling. Let's assume it's the context user
193  1 cUser = context.getUser();
194  1 job.setStringValue("contextUser", cUser);
195  1 jobNeedsUpdate = true;
196    }
197  4 String cLang = job.getStringValue("contextLang");
198  4 if (cLang.equals("")) {
199  1 cLang = context.getLanguage();
200  1 job.setStringValue("contextLang", cLang);
201  1 jobNeedsUpdate = true;
202    }
203  4 String iDb = context.getWikiId();
204  4 String cDb = job.getStringValue("contextDatabase");
205  4 if (cDb.equals("") || !cDb.equals(iDb)) {
206  1 cDb = context.getWikiId();
207  1 job.setStringValue("contextDatabase", cDb);
208  1 jobNeedsUpdate = true;
209    }
210   
211  4 if (jobNeedsUpdate) {
212  1 try {
213  1 context.setWikiId(cDb);
214  1 XWikiDocument jobHolder = context.getWiki().getDocument(job.getName(), context);
215  1 jobHolder.setMinorEdit(true);
216  1 context.getWiki().saveDocument(jobHolder, context);
217    } catch (XWikiException e) {
218  0 throw new SchedulerPluginException(
219    SchedulerPluginException.ERROR_SCHEDULERPLUGIN_UNABLE_TO_PREPARE_JOB_CONTEXT,
220    "Failed to prepare context for job with job name " + job.getStringValue("jobName"), e);
221    } finally {
222  1 context.setWikiId(iDb);
223    }
224    }
225   
226    // lets now build the stub context
227  4 XWikiContext scontext = context.clone();
228  4 scontext.setWiki(context.getWiki());
229  4 context.getWiki().getStore().cleanUp(context);
230   
231    // We are sure the context request is a real servlet request
232    // So we force the dummy request with the current host
233  4 XWikiServletRequestStub dummy = new XWikiServletRequestStub();
234  4 dummy.setHost(context.getRequest().getHeader("x-forwarded-host"));
235  4 dummy.setScheme(context.getRequest().getScheme());
236  4 dummy.setContextPath(context.getRequest().getContextPath());
237  4 XWikiServletRequest request = new XWikiServletRequest(dummy);
238  4 scontext.setRequest(request);
239   
240    // Force forged context response to a stub response, since the current context response
241    // will not mean anything anymore when running in the scheduler's thread, and can cause
242    // errors.
243  4 XWikiResponse stub = new XWikiServletResponseStub();
244  4 scontext.setResponse(stub);
245   
246    // feed the dummy context
247  4 scontext.setUser(cUser);
248  4 scontext.setLanguage(cLang);
249  4 scontext.setWikiId(cDb);
250  4 scontext.setMainXWiki(context.getMainXWiki());
251  4 if (scontext.getURL() == null) {
252  0 try {
253  0 scontext.setURL(new URL("http://www.mystuburl.com/"));
254    } catch (Exception e) {
255    // the URL is well formed, I promise
256    }
257    }
258   
259  4 com.xpn.xwiki.web.XWikiURLFactory xurf = context.getURLFactory();
260  4 if (xurf == null) {
261  3 xurf = context.getWiki().getURLFactoryService().createURLFactory(context.getMode(), context);
262    }
263  4 scontext.setURLFactory(xurf);
264   
265  4 try {
266  4 XWikiDocument cDoc = context.getWiki().getDocument(job.getDocumentReference(), context);
267  4 scontext.setDoc(cDoc);
268    } catch (Exception e) {
269  0 throw new SchedulerPluginException(
270    SchedulerPluginException.ERROR_SCHEDULERPLUGIN_UNABLE_TO_PREPARE_JOB_CONTEXT,
271    "Failed to prepare context for job with job name " + job.getStringValue("jobName"), e);
272    }
273   
274  4 return scontext;
275    }
276   
277    /**
278    * Restore the existing job, by looking up for such job in the database and re-scheduling those according to their
279    * stored status. If a Job is stored with the status "Normal", it is just scheduled If a Job is stored with the
280    * status "Paused", then it is both scheduled and paused. Jobs with other status (None, Complete) are not
281    * rescheduled.
282    *
283    * @param context The XWikiContext when initializing the plugin
284    */
 
285  2 toggle private void restoreExistingJobs(XWikiContext context)
286    {
287  2 String hql = ", BaseObject as obj where obj.name=doc.fullName and obj.className='XWiki.SchedulerJobClass'";
288  2 try {
289  2 List<DocumentReference> jobDocReferences =
290    context.getWiki().getStore().searchDocumentReferences(hql, context);
291  2 for (DocumentReference docReference : jobDocReferences) {
292  5 try {
293  5 XWikiDocument jobDoc = context.getWiki().getDocument(docReference, context);
294   
295  5 register(jobDoc, context);
296    } catch (Exception e) {
297  0 LOGGER.error("Failed to restore job with in document [{}] and wiki [{}]", docReference,
298    context.getWikiId(), e);
299    }
300    }
301    } catch (Exception e) {
302  0 LOGGER.error("Failed to restore existing scheduler jobs in wiki [{}]", context.getWikiId(), e);
303    }
304    }
305   
 
306  5 toggle private void register(XWikiDocument jobDoc, XWikiContext context) throws SchedulerPluginException
307    {
308  5 BaseObject jobObj = jobDoc.getXObject(XWIKI_JOB_CLASSREFERENCE);
309   
310  5 register(jobObj, context);
311    }
312   
 
313  7 toggle private void register(BaseObject jobObj, XWikiContext context) throws SchedulerPluginException
314    {
315  7 String status = jobObj.getStringValue("status");
316  7 if (status.equals(JobState.STATE_NORMAL) || status.equals(JobState.STATE_PAUSED)) {
317  3 scheduleJob(jobObj, context);
318    }
319  7 if (status.equals(JobState.STATE_PAUSED)) {
320  0 pauseJob(jobObj, context);
321    }
322    }
323   
 
324  1 toggle private void unregister(BaseObject jobObj, XWikiContext context) throws SchedulerPluginException
325    {
326  1 String status = jobObj.getStringValue("status");
327  1 if (status.equals(JobState.STATE_NORMAL) || status.equals(JobState.STATE_PAUSED)) {
328  0 scheduleJob(jobObj, context);
329    }
330  1 if (status.equals(JobState.STATE_PAUSED)) {
331  0 pauseJob(jobObj, context);
332    }
333    }
334   
335    /**
336    * Retrieve the job's status of a given {@link com.xpn.xwiki.plugin.scheduler.SchedulerPlugin#XWIKI_JOB_CLASS} job
337    * XObject, by asking the actual job status to the quartz scheduler instance. It's the actual status, as the one
338    * stored in the XObject may be changed manually by users.
339    *
340    * @param object the XObject to give the status of
341    * @return the status of the Job inside the quartz scheduler, as {@link com.xpn.xwiki.plugin.scheduler.JobState}
342    * instance
343    */
 
344  30 toggle public JobState getJobStatus(BaseObject object, XWikiContext context) throws SchedulerException
345    {
346  30 TriggerState state = getScheduler().getTriggerState(new TriggerKey(getObjectUniqueId(object, context)));
347  30 return new JobState(state);
348    }
349   
 
350  4 toggle public boolean scheduleJob(BaseObject object, XWikiContext context) throws SchedulerPluginException
351    {
352  4 boolean scheduled = true;
353  4 try {
354    // compute the job unique Id
355  4 String xjob = getObjectUniqueId(object, context);
356   
357    // Load the job class.
358    // Note: Remember to always use the current thread's class loader and not the container's
359    // (Class.forName(...)) since otherwise we will not be able to load classes installed with EM.
360  4 ClassLoader currentThreadClassLoader = Thread.currentThread().getContextClassLoader();
361  4 String jobClassName = object.getStringValue("jobClass");
362  4 Class<Job> jobClass = (Class<Job>) Class.forName(jobClassName, true, currentThreadClassLoader);
363   
364    // Build the new job.
365  4 JobBuilder jobBuilder = JobBuilder.newJob(jobClass);
366   
367  4 jobBuilder.withIdentity(xjob);
368  4 jobBuilder.storeDurably();
369   
370  4 JobDataMap data = new JobDataMap();
371   
372    // Let's prepare an execution context...
373  4 XWikiContext stubContext = prepareJobStubContext(object, context);
374  4 data.put("context", stubContext);
375  4 data.put("xcontext", stubContext);
376  4 data.put("xwiki", new com.xpn.xwiki.api.XWiki(context.getWiki(), stubContext));
377  4 data.put("xjob", object);
378  4 data.put("services", Utils.getComponent(ScriptServiceManager.class));
379   
380  4 jobBuilder.setJobData(data);
381   
382  4 getScheduler().addJob(jobBuilder.build(), true);
383   
384  4 TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
385   
386  4 triggerBuilder.withIdentity(xjob);
387  4 triggerBuilder.forJob(xjob);
388   
389  4 triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(object.getStringValue("cron")));
390   
391  4 Trigger trigger = triggerBuilder.build();
392   
393  4 JobState status = getJobStatus(object, context);
394   
395  4 switch (status.getQuartzState()) {
396  0 case PAUSED:
397    // a paused job must be resumed, not scheduled
398  0 break;
399  0 case NORMAL:
400  0 if (getTrigger(object, context).compareTo(trigger) != 0) {
401  0 LOGGER.debug("Reschedule Job: [{}]", object.getStringValue("jobName"));
402    }
403  0 getScheduler().rescheduleJob(trigger.getKey(), trigger);
404  0 break;
405  4 case NONE:
406  4 LOGGER.debug("Schedule Job: [{}]", object.getStringValue("jobName"));
407  4 getScheduler().scheduleJob(trigger);
408  4 LOGGER.info("XWiki Job Status: [{}]", object.getStringValue("status"));
409  4 if (object.getStringValue("status").equals("Paused")) {
410  0 getScheduler().pauseJob(new JobKey(xjob));
411  0 saveStatus("Paused", object, context);
412    } else {
413  4 saveStatus("Normal", object, context);
414    }
415  4 break;
416  0 default:
417  0 LOGGER.debug("Schedule Job: [{}]", object.getStringValue("jobName"));
418  0 getScheduler().scheduleJob(trigger);
419  0 saveStatus("Normal", object, context);
420  0 break;
421    }
422    } catch (SchedulerException e) {
423  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_SCHEDULE_JOB,
424    "Error while scheduling job " + object.getStringValue("jobName"), e);
425    } catch (ClassNotFoundException e) {
426  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_JOB_XCLASS_NOT_FOUND,
427    "Error while loading job class for job : " + object.getStringValue("jobName"), e);
428    } catch (XWikiException e) {
429  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_JOB_XCLASS_NOT_FOUND,
430    "Error while saving job status for job : " + object.getStringValue("jobName"), e);
431    }
432   
433  4 return scheduled;
434    }
435   
436    /**
437    * Pause the job with the given name by pausing all of its current triggers.
438    *
439    * @param object the non-wrapped XObject Job to be paused
440    */
 
441  1 toggle public void pauseJob(BaseObject object, XWikiContext context) throws SchedulerPluginException
442    {
443  1 String job = getObjectUniqueId(object, context);
444  1 try {
445  1 getScheduler().pauseJob(new JobKey(job));
446  1 saveStatus("Paused", object, context);
447    } catch (SchedulerException e) {
448  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_PAUSE_JOB,
449    "Error occured while trying to pause job " + object.getStringValue("jobName"), e);
450    } catch (XWikiException e) {
451  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_PAUSE_JOB,
452    "Error occured while trying to save status of job " + object.getStringValue("jobName"), e);
453    }
454    }
455   
456    /**
457    * Resume the job with the given name (un-pause)
458    *
459    * @param object the non-wrapped XObject Job to be resumed
460    */
 
461  1 toggle public void resumeJob(BaseObject object, XWikiContext context) throws SchedulerPluginException
462    {
463  1 String job = getObjectUniqueId(object, context);
464  1 try {
465  1 getScheduler().resumeJob(new JobKey(job));
466  1 saveStatus("Normal", object, context);
467    } catch (SchedulerException e) {
468  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_RESUME_JOB,
469    "Error occured while trying to resume job " + object.getStringValue("jobName"), e);
470    } catch (XWikiException e) {
471  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_RESUME_JOB,
472    "Error occured while trying to save status of job " + object.getStringValue("jobName"), e);
473    }
474    }
475   
476    /**
477    * Trigger a job (execute it now)
478    *
479    * @param object the non-wrapped XObject Job to be triggered
480    * @param context the XWiki context
481    */
 
482  2 toggle public void triggerJob(BaseObject object, XWikiContext context) throws SchedulerPluginException
483    {
484  2 String job = getObjectUniqueId(object, context);
485  2 try {
486  2 getScheduler().triggerJob(new JobKey(job));
487    } catch (SchedulerException e) {
488  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_TRIGGER_JOB,
489    "Error occured while trying to trigger job " + object.getStringValue("jobName"), e);
490    }
491    }
492   
493    /**
494    * Unschedule the given job
495    *
496    * @param object the unwrapped XObject job to be unscheduled
497    */
 
498  1 toggle public void unscheduleJob(BaseObject object, XWikiContext context) throws SchedulerPluginException
499    {
500  1 String job = getObjectUniqueId(object, context);
501  1 try {
502  1 getScheduler().deleteJob(new JobKey(job));
503  1 saveStatus("None", object, context);
504    } catch (SchedulerException e) {
505  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_JOB_XCLASS_NOT_FOUND,
506    "Error while unscheduling job " + object.getStringValue("jobName"), e);
507    } catch (XWikiException e) {
508  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_JOB_XCLASS_NOT_FOUND,
509    "Error while saving status of job " + object.getStringValue("jobName"), e);
510    }
511    }
512   
513    /**
514    * Get Trigger object of the given job
515    *
516    * @param object the unwrapped XObject to be retrieve the trigger for
517    * @param context the XWiki context
518    * @return the trigger object of the given job
519    */
 
520  16 toggle private Trigger getTrigger(BaseObject object, XWikiContext context) throws SchedulerPluginException
521    {
522  16 String job = getObjectUniqueId(object, context);
523  16 Trigger trigger;
524  16 try {
525  16 trigger = getScheduler().getTrigger(new TriggerKey(job));
526    } catch (SchedulerException e) {
527  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_JOB_XCLASS_NOT_FOUND,
528    "Error while getting trigger for job " + job, e);
529    }
530  16 if (trigger == null) {
531  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_JOB_DOES_NOT_EXITS,
532    "Job does not exists");
533    }
534   
535  16 return trigger;
536    }
537   
538    /**
539    * Give, for a BaseObject job in a {@link JobState#STATE_NORMAL} state, the previous date at which the job has been
540    * executed. Note that this method does not compute a date from the CRON expression, it only returns a date value
541    * which is set each time the job is executed. If the job has never been fired this method will return null.
542    *
543    * @param object unwrapped XObject job for which the next fire time will be given
544    * @param context the XWiki context
545    * @return the next Date the job will be fired at, null if the job has never been fired
546    */
 
547  0 toggle public Date getPreviousFireTime(BaseObject object, XWikiContext context) throws SchedulerPluginException
548    {
549  0 return getTrigger(object, context).getPreviousFireTime();
550    }
551   
552    /**
553    * Get the next fire time for the given job name SchedulerJob
554    *
555    * @param object unwrapped XObject job for which the next fire time will be given
556    * @return the next Date the job will be fired at
557    */
 
558  16 toggle public Date getNextFireTime(BaseObject object, XWikiContext context) throws SchedulerPluginException
559    {
560  16 return getTrigger(object, context).getNextFireTime();
561    }
562   
 
563  25 toggle @Override
564    public Api getPluginApi(XWikiPluginInterface plugin, XWikiContext context)
565    {
566  25 return new SchedulerPluginApi((SchedulerPlugin) plugin, context);
567    }
568   
 
569  20 toggle @Override
570    public String getName()
571    {
572  20 return "scheduler";
573    }
574   
575    /**
576    * @param scheduler the scheduler to use
577    */
 
578  2 toggle public void setScheduler(Scheduler scheduler)
579    {
580  2 this.scheduler = scheduler;
581    }
582   
583    /**
584    * @return the scheduler in use
585    */
 
586  65 toggle public Scheduler getScheduler()
587    {
588  65 return this.scheduler;
589    }
590   
591    /**
592    * @return the default Scheduler instance
593    * @throws SchedulerPluginException if the default Scheduler instance failed to be retrieved for any reason. Note
594    * that on the first call the default scheduler is also initialized.
595    */
 
596  2 toggle private synchronized Scheduler getDefaultSchedulerInstance() throws SchedulerPluginException
597    {
598  2 Scheduler scheduler;
599  2 try {
600  2 scheduler = StdSchedulerFactory.getDefaultScheduler();
601    } catch (SchedulerException e) {
602  0 throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_GET_SCHEDULER,
603    "Error getting default Scheduler instance", e);
604    }
605  2 return scheduler;
606    }
607   
608    /**
609    * Associates the scheduler with a StatusListener
610    *
611    * @throws SchedulerPluginException if the status listener failed to be set properly
612    */
 
613  2 toggle private void setStatusListener() throws SchedulerPluginException
614    {
615  2 StatusListener listener = new StatusListener();
616  2 try {
617  2 getScheduler().getListenerManager().addSchedulerListener(listener);
618  2 getScheduler().getListenerManager().addJobListener(listener);
619    } catch (SchedulerException e) {
620  0 throw new SchedulerPluginException(
621    SchedulerPluginException.ERROR_SCHEDULERPLUGIN_INITIALIZE_STATUS_LISTENER,
622    "Error while initializing the status listener", e);
623    }
624    }
625   
 
626  7 toggle private void saveStatus(String status, BaseObject object, XWikiContext context) throws XWikiException
627    {
628  7 XWikiDocument jobHolder = context.getWiki().getDocument(object.getDocumentReference(), context);
629    // We need to retrieve the object BaseObject the document again. Otherwise, modifications made to the
630    // BaseObject passed as argument will not be saved (XWikiDocument#getObject clones the document
631    // and returns the BaseObject from the clone)
632    // TODO refactor the plugin in order to stop passing BaseObject around, passing document references instead.
633  7 BaseObject job = jobHolder.getXObject(XWIKI_JOB_CLASSREFERENCE);
634  7 job.setStringValue("status", status);
635  7 jobHolder.setMinorEdit(true);
636  7 context.getWiki().saveDocument(jobHolder, context);
637    }
638   
639    /**
640    * Compute a cross-document unique {@link com.xpn.xwiki.objects.BaseObject} id, by concatenating its name (it's
641    * document holder full name, such as "SomeSpace.SomeDoc") and it's instance number inside this document.
642    * <p>
643    * The scheduler uses this unique object id to assure the unicity of jobs
644    *
645    * @return a unique String that can identify the object
646    */
 
647  55 toggle private String getObjectUniqueId(BaseObject object, XWikiContext context)
648    {
649  55 return context.getWikiId() + ":" + object.getName() + "_" + object.getNumber();
650    }
651   
 
652  2 toggle @Override
653    public List<Event> getEvents()
654    {
655  2 return EVENTS;
656    }
657   
 
658  72 toggle @Override
659    public void onEvent(Event event, Object source, Object data)
660    {
661  72 XWikiContext xcontext = (XWikiContext) data;
662  72 XWikiDocument document = (XWikiDocument) source;
663  72 XWikiDocument originalDocument = document.getOriginalDocument();
664   
665  72 BaseObject jobObj = document.getXObject(XWIKI_JOB_CLASSREFERENCE);
666  72 BaseObject originalJobObj = originalDocument.getXObject(XWIKI_JOB_CLASSREFERENCE);
667   
668  72 if (jobObj == null) {
669  60 if (originalJobObj != null) {
670    // Job deleted
671  1 try {
672  1 unregister(originalJobObj, xcontext);
673    } catch (SchedulerPluginException e) {
674  0 LOGGER.warn("Failed to register job in document [{}]: {}", document.getDocumentReference(),
675    ExceptionUtils.getRootCauseMessage(e));
676    }
677    }
678    } else {
679  12 if (originalJobObj == null) {
680    // New job
681  2 try {
682  2 register(jobObj, xcontext);
683    } catch (SchedulerPluginException e) {
684  0 LOGGER.warn("Failed to register job in document [{}]: {}", document.getDocumentReference(),
685    ExceptionUtils.getRootCauseMessage(e));
686    }
687    }
688    }
689    }
690    }