001package com.hfg.util.scheduler;
002
003import com.hfg.util.StringUtil;
004import com.hfg.util.collection.CollectionUtil;
005import com.hfg.util.collection.OrderedSet;
006
007import java.util.ArrayList;
008import java.util.Calendar;
009import java.util.Collections;
010import java.util.Date;
011import java.util.GregorianCalendar;
012import java.util.List;
013
014//------------------------------------------------------------------------------
015/**
016 Scheduler that uses the same specification syntax as the Unix cron utility.
017 See <a href='http://en.wikipedia.org/wiki/Cron'>Wikipedia</a>.
018 The text-based specification has five fields:
019 <pre>
020  * * * * *  command to execute
021  | | | | |
022  | | | | |
023  | | | | +------ day of week (0 - 6) (0 to 6 are Sunday to Saturday, or use names; 7 is Sunday, the same as 0)
024  | | | +----------- month (1 - 12)
025  | | +---------------- day of month (1 - 31)
026  | +--------------------- hour (0 - 23)
027  +-------------------------- min (0 - 59)
028 </pre>
029 <div>
030  @author J. Alex Taylor, hairyfatguy.com
031 </div>
032 */
033//------------------------------------------------------------------------------
034// com.hfg Library
035//
036// This library is free software; you can redistribute it and/or
037// modify it under the terms of the GNU Lesser General Public
038// License as published by the Free Software Foundation; either
039// version 2.1 of the License, or (at your option) any later version.
040//
041// This library is distributed in the hope that it will be useful,
042// but WITHOUT ANY WARRANTY; without even the implied warranty of
043// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
044// Lesser General Public License for more details.
045//
046// You should have received a copy of the GNU Lesser General Public
047// License along with this library; if not, write to the Free Software
048// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
049//
050// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
051// jataylor@hairyfatguy.com
052//------------------------------------------------------------------------------
053// TODO: More to do to fully support cron syntax.
054
055public class CronSchedule implements Schedule
056{
057   private String mMinuteSpec;
058   private String mHourSpec;
059   private String mDayOfMonthSpec;
060   private String mMonthSpec;
061   private String mDayOfWeekSpec;
062
063   private OrderedSet<Integer> mMinuteValues;
064   private OrderedSet<Integer> mHourValues;
065   private OrderedSet<Integer> mDayOfMonthValues;
066   private OrderedSet<Integer> mMonthValues;
067   private OrderedSet<Integer> mDayOfWeekValues;
068
069   //###########################################################################
070   // CONSTRUCTORS
071   //###########################################################################
072
073   //---------------------------------------------------------------------------
074   public CronSchedule(String inCronString)
075   {
076      parse(inCronString);
077   }
078
079   //###########################################################################
080   // PUBLIC METHODS
081   //###########################################################################
082
083   //---------------------------------------------------------------------------
084   public Date next()
085   {
086      return nextAfter(new Date());
087   }
088
089   //---------------------------------------------------------------------------
090   public Date nextAfter(Date inReferenceDate)
091   {
092      Calendar calendar = new GregorianCalendar();
093      calendar.setTime(inReferenceDate);
094
095      // Cron doesn't specify times with a precision below minutes
096      calendar.set(Calendar.MILLISECOND, 0);
097      calendar.set(Calendar.SECOND, 0);
098
099      calendar.add(Calendar.MINUTE, 1);
100
101      // Every minute value is valid for execution unless values have been specified
102      if (CollectionUtil.hasValues(mMinuteValues))
103      {
104         while (! mMinuteValues.contains(calendar.get(Calendar.MINUTE)))
105         {
106            calendar.add(Calendar.MINUTE, 1);
107         }
108      }
109
110      // Every hour value is valid for execution unless values have been specified
111      if (CollectionUtil.hasValues(mHourValues))
112      {
113         while (! mHourValues.contains(calendar.get(Calendar.HOUR_OF_DAY)))
114         {
115            calendar.add(Calendar.HOUR_OF_DAY, 1);
116
117            if (null == mMinuteValues)
118            {
119               // Set the minute value to zero
120               zeroCalendarField(calendar, Calendar.MINUTE);
121            }
122         }
123      }
124
125      if (CollectionUtil.hasValues(mDayOfMonthValues)
126          || CollectionUtil.hasValues(mDayOfWeekValues))
127      {
128         boolean modified = false;
129         while ((mDayOfMonthValues != null
130                 && ! mDayOfMonthValues.contains(calendar.get(Calendar.DAY_OF_MONTH)))
131                || (mDayOfWeekValues != null
132                    && ! mDayOfWeekValues.contains(calendar.get(Calendar.DAY_OF_WEEK))))
133         {
134            calendar.add(Calendar.DAY_OF_MONTH, 1);
135            modified = true;
136         }
137
138         if (modified)
139         {
140            if (null == mHourValues)
141            {
142               // Set the hour value to zero
143               zeroCalendarField(calendar, Calendar.HOUR_OF_DAY);
144            }
145
146            if (null == mMinuteValues)
147            {
148               // Set the minute value to zero
149               zeroCalendarField(calendar, Calendar.MINUTE);
150            }
151         }
152      }
153
154      if (CollectionUtil.hasValues(mMonthValues))
155      {
156         boolean modified = false;
157         while ((mMonthValues != null
158               && ! mMonthValues.contains(calendar.get(Calendar.MONTH) + 1))
159               || (mDayOfMonthValues != null
160               && ! mDayOfMonthValues.contains(calendar.get(Calendar.DAY_OF_MONTH)))
161               || (mDayOfWeekValues != null
162               && ! mDayOfWeekValues.contains(calendar.get(Calendar.DAY_OF_WEEK))))
163         {
164            modified = true;
165
166            // Are we already at the end of the month?
167            if (calendar.get(Calendar.DAY_OF_MONTH) == calendar.getActualMaximum(Calendar.DAY_OF_MONTH))
168            {
169               calendar.set(Calendar.DAY_OF_MONTH, 1);
170               calendar.add(Calendar.MONTH, 1);
171               while (! mMonthValues.contains(calendar.get(Calendar.MONTH) + 1))
172               {
173                  calendar.add(Calendar.MONTH, 1);
174               }
175            }
176            else
177            {
178               calendar.add(Calendar.DAY_OF_MONTH, 1);
179            }
180         }
181
182         if (modified)
183         {
184            if (null == mHourValues)
185            {
186               // Set the hour value to zero
187               zeroCalendarField(calendar, Calendar.HOUR_OF_DAY);
188            }
189
190            if (null == mMinuteValues)
191            {
192               // Set the minute value to zero
193               zeroCalendarField(calendar, Calendar.MINUTE);
194            }
195         }
196      }
197
198      return calendar.getTime();
199   }
200
201   //---------------------------------------------------------------------------
202   public CronSchedule setMinuteSpec(String inMinuteSpec)
203   {
204      mMinuteValues = null;
205
206      if (StringUtil.isSet(inMinuteSpec))
207      {
208         String spec = inMinuteSpec.trim();
209         if (! spec.equals("*"))
210         {
211            List<Integer> values = new ArrayList<Integer>(60);
212            String[] pieces = spec.split(",");
213            for (String piece : pieces)
214            {
215               int startValue;
216               int endValue;
217               int dashIdx = piece.indexOf("-");
218               if (dashIdx > 0)
219               {
220                  startValue = Integer.parseInt(piece.substring(0, dashIdx));
221                  endValue   = Integer.parseInt(piece.substring(dashIdx + 1));
222               }
223               else
224               {
225                  startValue = endValue = Integer.parseInt(piece);
226               }
227
228               for (int i = startValue; i <= endValue; i++)
229               {
230                  if (i < 0
231                        || i > 59)
232                  {
233                     throw new RuntimeException("Invalid minute value: " + StringUtil.singleQuote(i) + "! Valid value range: 0-59.");
234                  }
235
236                  values.add(i);
237               }
238            }
239
240            // Make sure the values are sorted low to high
241            Collections.sort(values);
242
243            mMinuteValues = new OrderedSet<Integer>(values);
244         }
245      }
246
247      return this;
248   }
249
250   //---------------------------------------------------------------------------
251   public CronSchedule setHourSpec(String inHourSpec)
252   {
253      mHourValues = null;
254
255      if (StringUtil.isSet(inHourSpec))
256      {
257         String spec = inHourSpec.trim();
258         if (! spec.equals("*"))
259         {
260            List<Integer> values = new ArrayList<Integer>(24);
261            String[] pieces = spec.split(",");
262            for (String piece : pieces)
263            {
264               int startValue;
265               int endValue;
266               int dashIdx = piece.indexOf("-");
267               if (dashIdx > 0)
268               {
269                  startValue = Integer.parseInt(piece.substring(0, dashIdx));
270                  endValue   = Integer.parseInt(piece.substring(dashIdx + 1));
271               }
272               else
273               {
274                  startValue = endValue = Integer.parseInt(piece);
275               }
276
277               for (int i = startValue; i <= endValue; i++)
278               {
279                  if (i < 0
280                      || i > 23)
281                  {
282                     throw new RuntimeException("Invalid hour value: " + StringUtil.singleQuote(i) + "! Valid value range: 0-23.");
283                  }
284
285                  values.add(i);
286               }
287            }
288
289            // Make sure the values are sorted low to high
290            Collections.sort(values);
291
292            mHourValues = new OrderedSet<Integer>(values);
293         }
294      }
295
296      return this;
297   }
298
299   //---------------------------------------------------------------------------
300   public CronSchedule setDayOfMonthSpec(String inDayOfMonthSpec)
301   {
302      mDayOfMonthValues = null;
303
304      if (StringUtil.isSet(inDayOfMonthSpec))
305      {
306         String spec = inDayOfMonthSpec.trim();
307         if (! spec.equals("*"))
308         {
309            List<Integer> values = new ArrayList<Integer>(32);
310            String[] pieces = spec.split(",");
311            for (String piece : pieces)
312            {
313               int startValue;
314               int endValue;
315               int dashIdx = piece.indexOf("-");
316               if (dashIdx > 0)
317               {
318                  startValue = Integer.parseInt(piece.substring(0, dashIdx));
319                  endValue   = Integer.parseInt(piece.substring(dashIdx + 1));
320               }
321               else
322               {
323                  startValue = endValue = Integer.parseInt(piece);
324               }
325
326               for (int i = startValue; i <= endValue; i++)
327               {
328                  if (i < 1
329                      || i > 31)
330                  {
331                     throw new RuntimeException("Invalid day of month value: " + StringUtil.singleQuote(i) + "! Valid value range: 1-31.");
332                  }
333
334                  values.add(i);
335               }
336            }
337
338            // Make sure the values are sorted low to high
339            Collections.sort(values);
340
341            mDayOfMonthValues = new OrderedSet<Integer>(values);
342         }
343      }
344
345      return this;
346   }
347
348   //---------------------------------------------------------------------------
349   public CronSchedule setMonthSpec(String inMonthSpec)
350   {
351      mMonthValues = null;
352
353      if (StringUtil.isSet(inMonthSpec))
354      {
355         String spec = inMonthSpec.trim();
356         if (! spec.equals("*"))
357         {
358            List<Integer> values = new ArrayList<Integer>(13);
359            String[] pieces = spec.split(",");
360            for (String piece : pieces)
361            {
362               int startValue;
363               int endValue;
364               int dashIdx = piece.indexOf("-");
365               if (dashIdx > 0)
366               {
367                  startValue = Integer.parseInt(piece.substring(0, dashIdx));
368                  endValue   = Integer.parseInt(piece.substring(dashIdx + 1));
369               }
370               else
371               {
372                  startValue = endValue = Integer.parseInt(piece);
373               }
374
375               for (int i = startValue; i <= endValue; i++)
376               {
377                  if (i < 1
378                      || i > 12)
379                  {
380                     throw new RuntimeException("Invalid day of month value: " + StringUtil.singleQuote(i) + "! Valid value range: 1-12.");
381                  }
382
383                  values.add(i);
384               }
385            }
386
387            // Make sure the values are sorted low to high
388            Collections.sort(values);
389
390            mMonthValues = new OrderedSet<Integer>(values);
391         }
392      }
393
394      return this;
395   }
396
397   //---------------------------------------------------------------------------
398   public CronSchedule setDayOfWeekSpec(String inDayOfWeekSpec)
399   {
400      mDayOfWeekValues = null;
401
402      if (StringUtil.isSet(inDayOfWeekSpec))
403      {
404         String spec = inDayOfWeekSpec.trim();
405         if (! spec.equals("*"))
406         {
407            List<Integer> values = new ArrayList<Integer>(13);
408            String[] pieces = spec.split(",");
409            for (String piece : pieces)
410            {
411               int startValue;
412               int endValue;
413               int dashIdx = piece.indexOf("-");
414               if (dashIdx > 0)
415               {
416                  startValue = Integer.parseInt(piece.substring(0, dashIdx));
417                  endValue   = Integer.parseInt(piece.substring(dashIdx + 1));
418               }
419               else
420               {
421                  startValue = endValue = Integer.parseInt(piece);
422               }
423
424               for (int i = startValue; i <= endValue; i++)
425               {
426                  if (i < 0
427                      || i > 6)
428                  {
429                     throw new RuntimeException("Invalid day of month value: " + StringUtil.singleQuote(i) + "! Valid value range: 0-6.");
430                  }
431
432                  values.add(i + 1);
433               }
434            }
435
436            // Make sure the values are sorted low to high
437            Collections.sort(values);
438
439            mDayOfWeekValues = new OrderedSet<Integer>(values);
440         }
441      }
442
443      return this;
444   }
445
446   //###########################################################################
447   // PRIVATE METHODS
448   //###########################################################################
449
450   //---------------------------------------------------------------------------
451   private void parse(String inCronString)
452   {
453      String[] pieces = inCronString.trim().split("\\s+");
454      if (pieces.length != 5)
455      {
456         throw new RuntimeException("The cron string " + StringUtil.singleQuote(inCronString) + " did not have the expected 5 cron fields!");
457      }
458
459      mMinuteSpec     = pieces[0];
460      mHourSpec       = pieces[1];
461      mDayOfMonthSpec = pieces[2];
462      mMonthSpec      = pieces[3];
463      mDayOfWeekSpec  = pieces[4];
464
465      setMinuteSpec(pieces[0]);
466      setHourSpec(pieces[1]);
467      setDayOfMonthSpec(pieces[2]);
468      setMonthSpec(pieces[3]);
469      setDayOfWeekSpec(pieces[4]);
470
471   }
472
473   //---------------------------------------------------------------------------
474   private void zeroCalendarField(Calendar inCalendar, int inField)
475   {
476      inCalendar.add(inField, - inCalendar.get(inField));
477   }
478
479}
480