View Javadoc

1   // 
2   //Copyright (c) 2003, Caltha - Gajda, Krzewski, Mach, Potempski Sp.J. 
3   //All rights reserved. 
4   //   
5   //Redistribution and use in source and binary forms, with or without modification,  
6   //are permitted provided that the following conditions are met: 
7   //   
8   //* Redistributions of source code must retain the above copyright notice,  
9   //this list of conditions and the following disclaimer. 
10  //* Redistributions in binary form must reproduce the above copyright notice,  
11  //this list of conditions and the following disclaimer in the documentation  
12  //and/or other materials provided with the distribution. 
13  //* Neither the name of the Caltha - Gajda, Krzewski, Mach, Potempski Sp.J.  
14  //nor the names of its contributors may be used to endorse or promote products  
15  //derived from this software without specific prior written permission. 
16  // 
17  //THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"  
18  //AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED  
19  //WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
20  //IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,  
21  //INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,  
22  //BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
23  //OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,  
24  //WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)  
25  //ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE  
26  //POSSIBILITY OF SUCH DAMAGE. 
27  //
28  
29  package org.objectledge.scheduler;
30  
31  import java.io.StringReader;
32  import java.util.Calendar;
33  import java.util.Date;
34  import java.util.GregorianCalendar;
35  
36  import org.objectledge.i18n.I18n;
37  import org.objectledge.scheduler.cron.CronParser;
38  import org.objectledge.scheduler.cron.ParseException;
39  import org.objectledge.scheduler.cron.Token;
40  import org.objectledge.scheduler.cron.ValueOutOfRangeException;
41  
42  /**
43   * A schedule that follows the configuration syntax of Unix cron utility. 
44   *
45   * <p>The configuration grammar:</p>
46   * <pre>
47   * schedule_config := ( ( minute hour day month weekday ) | special )
48   * special := ( "@reboot" | "@yearly" | "@annually" | "@monthly" | "@weekly" |
49   *   "@daily" | "@midnight" | "@hourly" )
50   * minute := value_spec
51   * hour := value_spec
52   * day := value_spec
53   * month := ( value_spec | month_name )
54   * weekday := ( value_spec | weekday_name )
55   * month_name := ( "jan" | "feb" | "mar" | "apr" | "may" | "jun" |
56   *   "jul" | "aug" | "sep" | "oct" | "nov" | "dec" )
57   * weekday_name := ( "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun" )
58   * value_spec := ( "*" | list_of_values )
59   * list_of_values := ( value_or_range ( "," value_or_range )* )
60   * value_or_range := ( number | range )
61   * range := number "-" number [ "/" number ]
62   * number := LEXICAL( [0-9]+ )
63   * </pre>
64   *
65   * <p>Configuration syntax is compatible with Paul Vixie's <a
66   * href="ftp://ftp.vix.com/pub/vixie/cron-3.0">cron</a>, version 3.0.</p> 
67   */
68  public class CronSchedule
69      implements Schedule
70  {
71      // constants /////////////////////////////////////////////////////////////
72  
73      /*** index of minute field. */
74      private static final int MINUTE = 0;
75      
76      /*** index of hour field. */
77      private static final int HOUR_OF_DAY = 1;
78      
79      /*** index of day field. */
80      private static final int DAY_OF_MONTH = 2;
81      
82      /*** index of month field. */
83      private static final int MONTH = 3;
84      
85      /*** index of weekday field. */
86      private static final int DAY_OF_WEEK = 4;
87  
88      /*** The schedule type. */
89      public static final String TYPE = "cron";
90  
91      // instance variables ////////////////////////////////////////////////////
92      
93      /*** The textual configuration form. */
94      private String config;
95  
96      /*** The parserd schedule data. */
97      private int[][] schedule;
98      
99      /*** I18n component */
100     private I18n i18n;
101     
102     /***
103      * Constructor.
104      * 
105      * @param i18n the i18n component.
106      */
107     public CronSchedule(I18n i18n)
108     {
109         this.i18n = i18n;
110     }
111     
112     /***
113      * {@inheritDoc}
114      */
115     public void init(AbstractScheduler scheduler, String config)
116         throws InvalidScheduleException
117     {
118         setConfig(config);
119     }
120 
121     /***
122      * {@inheritDoc}
123      */
124     public String getType()
125     {
126         return TYPE;
127     }
128 
129     /***
130      * {@inheritDoc}
131      */
132     public String getConfig()
133     {
134         return config;
135     }
136 
137     /***
138      * {@inheritDoc}
139      */
140     public void setConfig(String config)
141         throws InvalidScheduleException
142     {
143         //TODO Add Polling here...
144         StringReader sr = new StringReader("");
145         CronParser parser = new CronParser(sr);
146         try
147         {
148             this.schedule = parser.parse(config);
149             this.config = config;
150         }
151         catch(ParseException e)
152         {
153             throw new InvalidScheduleException(localizeParseException(e), e);
154         }
155         catch(ValueOutOfRangeException e)
156         {
157             throw new InvalidScheduleException(localizeValueOutOrRangeException(e), e);
158         }
159         //TODO recycle parser
160     }
161 
162     /***
163      * {@inheritDoc}
164      */
165     public boolean atStartup()
166     {
167         return (schedule.length == 0);
168     }
169 
170     /***
171      * {@inheritDoc}
172      */
173     public Date getNextRunTime(Date currentTime, Date lastRunTime)
174     {
175         if(schedule.length == 0)
176         {
177             return null;
178         }
179         else
180         {
181             GregorianCalendar nextRun = new GregorianCalendar();
182             nextRun.setTime(currentTime);
183             // skip to next full minute
184             nextRun.set(Calendar.MILLISECOND, 0);
185             nextRun.set(Calendar.SECOND, 0);
186             nextRun.add(Calendar.MINUTE, 1);
187 
188             computeDay(nextRun);
189             computeHour(nextRun);
190             computeMinute(nextRun);
191                         
192             return nextRun.getTime();
193         }
194     }
195 
196     // implementation ////////////////////////////////////////////////////////
197 
198     private void computeMonth(Calendar nextRunDOM)
199     {
200         int i;
201         if(schedule[MONTH].length != 0)
202         {
203             loop: for(i=0; i<schedule[MONTH].length; i++)
204             {
205                 if(schedule[MONTH][i] >= nextRunDOM.get(Calendar.MONTH))
206                 {
207                     break loop;
208                 }
209             }
210             if(i == schedule[MONTH].length)
211             {
212                 nextRunDOM.add(Calendar.YEAR, 1);
213                 nextRunDOM.set(Calendar.MONTH, schedule[MONTH][0]);
214                 nextRunDOM.set(Calendar.DAY_OF_MONTH, 0);
215                 nextRunDOM.set(Calendar.HOUR_OF_DAY, 0);
216                 nextRunDOM.set(Calendar.MINUTE, 0);
217             }
218             else
219             {
220                 if(schedule[MONTH][i] > nextRunDOM.get(Calendar.MONTH))
221                 {
222                     nextRunDOM.set(Calendar.MONTH, schedule[MONTH][i]);
223                     nextRunDOM.set(Calendar.DAY_OF_MONTH, 0);
224                     nextRunDOM.set(Calendar.HOUR_OF_DAY, 0);
225                     nextRunDOM.set(Calendar.MINUTE, 0);
226                 }
227             }
228         }        
229     }
230 
231     private void computeDay(Calendar nextRun)
232     {
233         int i;
234         GregorianCalendar nextRunDOM = new GregorianCalendar();
235         nextRunDOM.setTime(nextRun.getTime());
236 
237         computeMonth(nextRunDOM);
238         
239         // day of month
240         if(schedule[DAY_OF_MONTH].length != 0)
241         {
242             loop: for(i=0; i<schedule[DAY_OF_MONTH].length; i++)
243             {
244                 if(schedule[DAY_OF_MONTH][i] >= nextRunDOM.get(Calendar.DAY_OF_MONTH))
245                 {
246                     break loop;
247                 }
248             }
249             if(i == schedule[DAY_OF_MONTH].length)
250             {
251                 nextRunDOM.add(Calendar.MONTH, 1);
252                 computeMonth(nextRunDOM);
253                 // FIXME day > month length
254                 nextRunDOM.set(Calendar.DAY_OF_MONTH, schedule[DAY_OF_MONTH][0]);
255                 nextRunDOM.set(Calendar.HOUR_OF_DAY, 0);
256                 nextRunDOM.set(Calendar.MINUTE, 0);
257             }
258             else
259             {
260                 if(schedule[DAY_OF_MONTH][i] > nextRunDOM.get(Calendar.DAY_OF_MONTH))
261                 {
262                     nextRunDOM.set(Calendar.DAY_OF_MONTH, schedule[DAY_OF_MONTH][i]);
263                     // FIXME day > month length
264                     nextRunDOM.set(Calendar.HOUR_OF_DAY, 0);
265                     nextRunDOM.set(Calendar.MINUTE, 0);
266                 }
267             }
268         }
269 
270         GregorianCalendar nextRunDOW = new GregorianCalendar();
271         nextRunDOW.setTime(nextRun.getTime());
272         // day of week
273         if(schedule[DAY_OF_WEEK].length != 0)
274         {
275             loop: for(i=0; i<schedule[DAY_OF_WEEK].length; i++)
276             {
277                 if(schedule[DAY_OF_WEEK][i] >= nextRunDOW.get(Calendar.DAY_OF_WEEK))
278                 {
279                     break loop;
280                 }
281             }
282             if(i == schedule[DAY_OF_WEEK].length)
283             {
284                 nextRunDOW.add(Calendar.WEEK_OF_YEAR, 1);
285                 nextRunDOW.set(Calendar.DAY_OF_WEEK, schedule[DAY_OF_WEEK][0]);
286                 nextRunDOW.set(Calendar.HOUR_OF_DAY, 0);
287                 nextRunDOW.set(Calendar.MINUTE, 0);
288             }
289             else
290             {
291                 if(schedule[DAY_OF_WEEK][i] > nextRunDOW.get(Calendar.DAY_OF_WEEK))
292                 {
293                     if(nextRunDOW.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY &&
294                        nextRunDOW.getFirstDayOfWeek() != Calendar.SUNDAY)
295                     {
296                         nextRunDOW.add(Calendar.WEEK_OF_YEAR, 1);
297                     }   
298                     nextRunDOW.set(Calendar.DAY_OF_WEEK, schedule[DAY_OF_WEEK][i]);
299                     nextRunDOW.set(Calendar.HOUR_OF_DAY, 0);
300                     nextRunDOW.set(Calendar.MINUTE, 0);
301                 }
302             }
303         }
304         
305         if(schedule[DAY_OF_WEEK].length == 0)
306         {
307             nextRun.setTime(nextRunDOM.getTime());
308         }
309         else
310         {
311             if(schedule[DAY_OF_MONTH].length == 0 && schedule[MONTH].length == 0)
312             {
313                 nextRun.setTime(nextRunDOW.getTime());
314             }
315             else
316             {
317                 if(nextRunDOM.getTime().compareTo(nextRunDOW.getTime()) < 0)
318                 {
319                     nextRun.setTime(nextRunDOM.getTime());
320                 }
321                 else
322                 {
323                     nextRun.setTime(nextRunDOW.getTime());
324                 }
325             }    
326         }
327     }
328 
329     private void computeHour(Calendar nextRun)
330     {
331         int i;
332         if(schedule[HOUR_OF_DAY].length != 0)
333         {
334             loop: for(i=0; i<schedule[HOUR_OF_DAY].length; i++)
335             {
336                 if(schedule[HOUR_OF_DAY][i] >= nextRun.get(Calendar.HOUR_OF_DAY))
337                 {
338                     break loop;
339                 }
340             }
341             if(i == schedule[HOUR_OF_DAY].length)
342             {
343                 nextRun.add(Calendar.DAY_OF_MONTH, 1);
344                 computeDay(nextRun);
345                 nextRun.set(Calendar.HOUR_OF_DAY, schedule[HOUR_OF_DAY][0]);
346                 nextRun.set(Calendar.MINUTE, 0);
347             }
348             else
349             {
350                 if(schedule[HOUR_OF_DAY][i] > nextRun.get(Calendar.HOUR_OF_DAY))
351                 {
352                     nextRun.set(Calendar.MINUTE, 0);
353                     nextRun.set(Calendar.HOUR_OF_DAY, schedule[HOUR_OF_DAY][i]);
354                 }
355             }
356         }
357     }
358 
359     private void computeMinute(Calendar nextRun)
360     {
361         int i;
362         if(schedule[MINUTE].length != 0)
363         {
364             loop: for(i=0; i<schedule[MINUTE].length; i++)
365             {
366                 if(schedule[MINUTE][i] >= nextRun.get(Calendar.MINUTE))
367                 {
368                     break loop;
369                 }
370             }
371             if(i == schedule[MINUTE].length)
372             {
373                 nextRun.add(Calendar.HOUR_OF_DAY, 1);
374                 computeHour(nextRun);
375                 nextRun.set(Calendar.MINUTE, schedule[MINUTE][0]);
376             }
377             else
378             {
379                 if(schedule[MINUTE][i] > nextRun.get(Calendar.MINUTE))
380                 {
381                     nextRun.set(Calendar.MINUTE, schedule[MINUTE][i]);
382                 }
383             }
384         }
385     }
386 
387     /***
388      * The end of line string for this machine.
389      */
390     protected String eol = System.getProperty("line.separator", "\n");
391 
392     /***
393      * Create a localized message out of ParseException object.
394      *
395      * @param ex the exception.
396      */
397     private String localizeParseException(ParseException ex)
398     {
399         String expected = "";
400         int maxSize = 0;
401         for (int i = 0; i < ex.expectedTokenSequences.length; i++) 
402         {
403             if (maxSize < ex.expectedTokenSequences[i].length) 
404             {
405                 maxSize = ex.expectedTokenSequences[i].length;
406             }
407             for (int j = 0; j < ex.expectedTokenSequences[i].length; j++) 
408             {
409                 expected += ex.tokenImage[ex.expectedTokenSequences[i][j]] + " ";
410             }
411             if (ex.expectedTokenSequences[i][ex.expectedTokenSequences[i].length - 1] != 0) 
412             {
413                 expected += "...";
414             }
415             expected += eol + "    ";
416         }
417 
418         String encountered = "";
419         Token tok = ex.currentToken.next;
420         for (int i = 0; i < maxSize; i++) 
421         {
422             if (i != 0)
423             {
424                 encountered += " "; 
425             } 
426             if (tok.kind == 0) 
427             {
428                 encountered += ex.tokenImage[0];
429                 break;
430             }
431             encountered += addEscapes(tok.image);
432             tok = tok.next; 
433         }
434 
435         String[] strings = new String[] { 
436             encountered, 
437             Integer.toString(ex.currentToken.next.beginColumn), 
438             expected 
439         };
440                 
441         String pattern;
442         if(ex.expectedTokenSequences.length == 1)
443         {
444             pattern = "ledge.scheduler.cron.parseOne";
445         }
446         else
447         {
448             pattern = "ledge.scheduler.cron.parseMany";
449         }
450         return i18n.get(i18n.getDefaultLocale(), pattern, strings);
451     }
452 
453     /***
454      * Create a localized message out of ParseException object.
455      *
456      * @param ex the exception.
457      */
458     private String localizeValueOutOrRangeException(ValueOutOfRangeException ex)
459     {
460         String[] strings = new String[] { 
461             Integer.toString(ex.token.beginColumn), 
462             Integer.toString(ex.value),
463             Integer.toString(ex.min),
464             Integer.toString(ex.max)
465         };
466         String pattern = "ledge.scheduler.cron.outOfRange";
467         return i18n.get(i18n.getDefaultLocale(), pattern, strings);
468     }
469 
470     /***
471      * Used to convert raw characters to their escaped version
472      * when these raw version cannot be used as part of an ASCII
473      * string literal.
474      *
475      * <p>This method was copied from JavaCC generated code.</p>
476      *
477      * @author JavaCC team
478      * @param str input string.
479      * @return string with escaped sepcial characters.
480      */
481     protected String addEscapes(String str) 
482     {
483         StringBuilder retval = new StringBuilder();
484         char ch;
485         for (int i = 0; i < str.length(); i++) 
486         {
487             switch (str.charAt(i))
488             {
489             case 0 :
490                 continue;
491             case '\b':
492                 retval.append("//b");
493                 continue;
494             case '\t':
495                 retval.append("//t");
496                 continue;
497             case '\n':
498                 retval.append("//n");
499                 continue;
500             case '\f':
501                 retval.append("//f");
502                 continue;
503             case '\r':
504                 retval.append("//r");
505                 continue;
506             case '\"':
507                 retval.append("//\"");
508                 continue;
509             case '\'':
510                 retval.append("//\'");
511                 continue;
512             case '//':
513                 retval.append("////");
514                 continue;
515             default:
516                 if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) 
517                 {
518                     String s = "0000" + Integer.toString(ch, 16);
519                     retval.append("//u" + s.substring(s.length() - 4, s.length()));
520                 } 
521                 else 
522                 {
523                     retval.append(ch);
524                 }
525                 continue;
526             }
527         }
528         return retval.toString();
529     }
530 }