View Javadoc

1   /*
2    * Copyright (C) 2007 ETH Zurich
3    *
4    * This file is part of Fosstrak (www.fosstrak.org).
5    *
6    * Fosstrak is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License version 2.1, as published by the Free Software Foundation.
9    *
10   * Fosstrak 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 Fosstrak; if not, write to the Free
17   * Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18   * Boston, MA  02110-1301  USA
19   */
20  
21  package org.fosstrak.epcis.repository.query;
22  
23  import static java.util.Calendar.DAY_OF_MONTH;
24  import static java.util.Calendar.DAY_OF_WEEK;
25  import static java.util.Calendar.HOUR_OF_DAY;
26  import static java.util.Calendar.MINUTE;
27  import static java.util.Calendar.MONTH;
28  import static java.util.Calendar.SECOND;
29  import static java.util.Calendar.YEAR;
30  
31  import java.io.Serializable;
32  import java.util.GregorianCalendar;
33  import java.util.NoSuchElementException;
34  import java.util.TreeSet;
35  
36  import org.fosstrak.epcis.model.ImplementationException;
37  import org.fosstrak.epcis.model.QuerySchedule;
38  import org.fosstrak.epcis.model.SubscriptionControlsException;
39  import org.fosstrak.epcis.soap.ImplementationExceptionResponse;
40  import org.fosstrak.epcis.soap.SubscriptionControlsExceptionResponse;
41  import org.apache.commons.logging.Log;
42  import org.apache.commons.logging.LogFactory;
43  
44  /**
45   * This is a simple schedule which can return a "next scheduled time" after now
46   * or a given time. It is meant to be instantiated with a EPCIS QuerySchedule
47   * but could be extended to be used otherwise.
48   * 
49   * @author Arthur van Dorp
50   * @author Marco Steybe
51   */
52  public class Schedule implements Serializable {
53  
54      private static final Log LOG = LogFactory.getLog(Schedule.class);
55  
56      /**
57       * Auto-generated UID for serialization.
58       */
59      private static final long serialVersionUID = -2930237937444822557L;
60  
61      /**
62       * The valid second-values. Caveat: Empty means all seconds are valid.
63       */
64      private TreeSet<Integer> seconds = new TreeSet<Integer>();
65      private TreeSet<Integer> minutes = new TreeSet<Integer>();
66      private TreeSet<Integer> hours = new TreeSet<Integer>();
67      private TreeSet<Integer> daysOfMonth = new TreeSet<Integer>();
68      // months are 0-based
69      private TreeSet<Integer> months = new TreeSet<Integer>();
70      private TreeSet<Integer> daysOfWeek = new TreeSet<Integer>();
71  
72      /**
73       * Parameterless constructor for use with serialization.
74       */
75      Schedule() {
76      }
77  
78      /**
79       * Constructor for creating a new schedule according to the parameters in
80       * the given QuerySchedule 'schedule'.
81       * 
82       * @param schedule
83       *            The EPCIS style schedule to be used for constructing this
84       *            schedule.
85       * @throws SubscriptionControlsException
86       *             If invalid data is part of the Schedule.
87       */
88      public Schedule(final QuerySchedule schedule) throws SubscriptionControlsExceptionResponse {
89  
90          // TODO: remove (this is only required for conformance tests)
91          if ("1".equals(schedule.getMinute()) && schedule.getSecond() == null && schedule.getHour() == null
92                  && schedule.getDayOfMonth() == null && schedule.getMonth() == null && schedule.getDayOfWeek() == null) {
93              throw new SubscriptionControlsExceptionResponse("Invalid query schedule: schedule is set to every second");
94          }
95  
96          // ease handling of null values in the query schedule
97          if (schedule.getSecond() == null) {
98              schedule.setSecond("");
99          }
100         if (schedule.getMinute() == null) {
101             schedule.setMinute("");
102         }
103         if (schedule.getHour() == null) {
104             schedule.setHour("");
105         }
106         if (schedule.getDayOfMonth() == null) {
107             schedule.setDayOfMonth("");
108         }
109         if (schedule.getMonth() == null) {
110             schedule.setMonth("");
111         }
112         if (schedule.getDayOfWeek() == null) {
113             schedule.setDayOfWeek("");
114         }
115 
116         // retrieve the values from the given query schedule
117         String[] second = schedule.getSecond().split(",");
118         String[] minute = schedule.getMinute().split(",");
119         String[] hour = schedule.getHour().split(",");
120         String[] dayOfMonth = schedule.getDayOfMonth().split(",");
121         String[] month = schedule.getMonth().split(",");
122         String[] dayOfWeek = schedule.getDayOfWeek().split(",");
123 
124         // parse numbers and ranges, check and add values
125         handleValues(second, "second", 0, 59);
126         handleValues(minute, "minute", 0, 59);
127         handleValues(hour, "hour", 0, 23);
128         handleValues(dayOfMonth, "dayOfMonth", 1, 31);
129         // months given in QuerySchedule are 1-based
130         // but month values held in global variable "months" are 0-based!
131         handleValues(month, "month", 1, 12);
132         handleValues(dayOfWeek, "dayOfWeek", 1, 7);
133 
134         // check for invalid month/dayOfMonth combinations, e.g. 30.2., 31.4.
135         if (!months.isEmpty()
136                 && (months.first() == months.last() && months.first().intValue() == 1 && (daysOfMonth.first().intValue() == 30 || daysOfMonth.first().intValue() == 31))) {
137             throw new SubscriptionControlsExceptionResponse(
138                     "Invalid query schedule: impossible month/dayOfMonth combination, e.g. February 30.");
139         }
140         if (!months.isEmpty()
141                 && daysOfMonth.first().intValue() == 31
142                 && !months.contains(Integer.valueOf(0)) // months w. 31 days are
143                 // always ok
144                 && !months.contains(Integer.valueOf(2)) && !months.contains(Integer.valueOf(4))
145                 && !months.contains(Integer.valueOf(6)) && !months.contains(Integer.valueOf(7))
146                 && !months.contains(Integer.valueOf(9)) && !months.contains(Integer.valueOf(11))) {
147             throw new SubscriptionControlsExceptionResponse(
148                     "Invalid query schedule: impossible month/dayOfMonth combination, e.g. April 31.");
149         }
150     }
151 
152     /**
153      * Calculates the next scheduled time after now.
154      * 
155      * @return The next scheduled time after now.
156      * @throws ImplementationException
157      *             Almost any kind of error.
158      */
159     public GregorianCalendar nextScheduledTime() throws ImplementationExceptionResponse {
160         GregorianCalendar cal = new GregorianCalendar();
161         // start at the next second to avoid multiple results
162         cal.add(SECOND, 1);
163         return nextScheduledTime(cal);
164     }
165 
166     /**
167      * Calculates the next scheduled time after the given time. Algorithm idea:<br> -
168      * start with biggest time unit (i.e. year) of the given time <br> - if the
169      * time unit is valid (e.g. the time unit matches the <br>
170      * scheduled time, this is implicitly true if the time in the <br>
171      * schedule was omitted) *and* there exists a valid smaller time <br>
172      * unit, *then* return this time unit <br> - do this recursively for all
173      * time units <br> - month needs to be special cased because of dayOfWeek
174      * 
175      * @param time
176      *            Time after which next scheduled time should be returned.
177      * @return The next scheduled time after 'time'.
178      * @throws ImplementationException
179      *             Almost any kind of error.
180      */
181     public GregorianCalendar nextScheduledTime(final GregorianCalendar time) throws ImplementationExceptionResponse {
182         GregorianCalendar nextSchedule = (GregorianCalendar) time.clone();
183         // look at year
184         while (!monthMadeValid(nextSchedule)) {
185             nextSchedule.roll(YEAR, true);
186             setFieldsToMinimum(nextSchedule, MONTH);
187 
188         }
189         return nextSchedule;
190     }
191 
192     /**
193      * Returns true if the month and all smaller time units have been
194      * successfully set to valid values.
195      * 
196      * @param nextSchedule
197      *            The current candidate for the result.
198      * @return True if month and smaller units successfully set to valid values.
199      * @throws ImplementationException
200      *             Almost any kind of error.
201      */
202     private boolean monthMadeValid(final GregorianCalendar nextSchedule) throws ImplementationExceptionResponse {
203         // check if the month of the current time is valid, i.e. there is a
204         // month value in the schedule equal to the month value of the current
205         // time
206         while (!months.isEmpty() && !months.contains(Integer.valueOf(nextSchedule.get(MONTH)))) {
207             // no, month value of the current time is invalid
208             // roll the month (set it to the next value)
209             if (!setFieldToNextValidRoll(nextSchedule, MONTH, DAY_OF_MONTH)) {
210                 return false;
211             }
212         }
213         // now we're in a valid month, make smaller units valid as well or go to
214         // next month
215         while (!dayMadeValid(nextSchedule)) {
216             // no valid day for this month, try next
217             if (!setFieldToNextValidRoll(nextSchedule, MONTH, DAY_OF_MONTH)) {
218                 return false;
219             }
220             // reset all smaller units to minimum
221             if (!setFieldsToMinimum(nextSchedule, DAY_OF_MONTH)) {
222                 return false;
223             }
224         }
225         return true;
226     }
227 
228     /**
229      * Returns true if the day and all smaller units have been successfully set
230      * to valid values within the set month.
231      * 
232      * @param nextSchedule
233      *            The current candidate for the result.
234      * @return True if day and smaller units successfully set to valid values.
235      * @throws ImplementationException
236      *             Almost any kind of error.
237      */
238     private boolean dayMadeValid(final GregorianCalendar nextSchedule) throws ImplementationExceptionResponse {
239         if (!daysOfMonth.contains(Integer.valueOf(nextSchedule.get(DAY_OF_MONTH))) && !daysOfMonth.isEmpty()) {
240             if (!setFieldToNextValidRoll(nextSchedule, DAY_OF_MONTH, HOUR_OF_DAY)) {
241                 return false;
242             }
243         }
244 
245         // Check and make this also a valid day of week.
246         while (!daysOfWeek.contains(Integer.valueOf(nextSchedule.get(DAY_OF_WEEK))) && !daysOfWeek.isEmpty()) {
247             if (!setFieldToNextValidRoll(nextSchedule, DAY_OF_MONTH, HOUR_OF_DAY)) {
248                 return false;
249             } else if (!daysOfWeek.contains(Integer.valueOf(nextSchedule.get(DAY_OF_WEEK)))) {
250                 dayMadeValid(nextSchedule);
251             }
252         }
253 
254         // Now we're in a valid day, make smaller units
255         // valid as well or go to next day.
256         while (!hourMadeValid(nextSchedule)) {
257             // No valid hour for this day, try next day.
258             if (!setFieldToNextValidRoll(nextSchedule, DAY_OF_MONTH, HOUR_OF_DAY)) {
259                 return false;
260             }
261             // Reset all smaller units to min.
262             if (!setFieldsToMinimum(nextSchedule, HOUR_OF_DAY)) {
263                 return false;
264             }
265         }
266         return true;
267     }
268 
269     /**
270      * Returns true if the hour and all smaller units have been successfully set
271      * to valid values within the set day.
272      * 
273      * @param nextSchedule
274      *            The current candidate for the result.
275      * @return True if hour and smaller units successfully set to valid values.
276      * @throws ImplementationException
277      *             Almost any error.
278      */
279     private boolean hourMadeValid(final GregorianCalendar nextSchedule) throws ImplementationExceptionResponse {
280         if (!hours.contains(Integer.valueOf(nextSchedule.get(HOUR_OF_DAY))) && !hours.isEmpty()) {
281             if (!setFieldToNextValidRoll(nextSchedule, HOUR_OF_DAY, MINUTE)) {
282                 return false;
283             }
284         }
285 
286         // Now we're in a valid hour, make smaller units
287         // valid as well or go to next hour.
288         while (!minuteMadeValid(nextSchedule)) {
289             // No valid minute for this hour, try next hour.
290             if (!setFieldToNextValidRoll(nextSchedule, HOUR_OF_DAY, MINUTE)) {
291                 return false;
292             }
293             // Reset all smaller units to min.
294             if (!setFieldsToMinimum(nextSchedule, MINUTE)) {
295                 return false;
296             }
297         }
298         return true;
299     }
300 
301     /**
302      * Returns true if the minute and all smaller units have been successfully
303      * set to valid values within the set hour.
304      * 
305      * @param nextSchedule
306      *            The current candidate for the result.
307      * @return True if minute and smaller units successfully set to valid
308      *         values.
309      * @throws ImplementationException
310      *             Almost any error.
311      */
312     private boolean minuteMadeValid(final GregorianCalendar nextSchedule) throws ImplementationExceptionResponse {
313         if (!minutes.contains(Integer.valueOf(nextSchedule.get(MINUTE))) && !minutes.isEmpty()) {
314 
315             if (!setFieldToNextValidRoll(nextSchedule, MINUTE, SECOND)) {
316                 return false;
317             }
318         }
319 
320         // Now we're in a valid minute, make smaller units
321         // valid as well or go to next minute.
322         while (!secondMadeValid(nextSchedule)) {
323             // No valid second for this minute, try next minute.
324 
325             if (!setFieldToNextValidRoll(nextSchedule, MINUTE, SECOND)) {
326                 return false;
327             }
328             // Reset all smaller units to min.
329             if (!setFieldToMinimum(nextSchedule, SECOND)) {
330                 return false;
331             }
332         }
333         return true;
334     }
335 
336     /**
337      * Returns true if the second have been successfully set to valid values
338      * within the set minute.
339      * 
340      * @param nextSchedule
341      *            The current candidate for the result.
342      * @return True if second successfully set to valid values.
343      * @throws ImplementationException
344      *             Almost any error.
345      */
346     private boolean secondMadeValid(final GregorianCalendar nextSchedule) throws ImplementationExceptionResponse {
347         // check whether the second value of the current time is a valid
348         // scheduled second
349         if (!seconds.isEmpty() && !seconds.contains(Integer.valueOf(nextSchedule.get(SECOND)))) {
350             // no current second is not scheduled
351             // set is to the next scheduled second
352             return setToNextScheduledValue(nextSchedule, SECOND);
353         }
354         return true;
355     }
356 
357     /**
358      * Sets the specified field of the given callendar to the next scheduled
359      * value. Returns whether the new value has been set and is valid.
360      * 
361      * @param cal
362      *            Calendar to adjust.
363      * @param field
364      *            Field to adjust.
365      * @return Returns whether the new value has been set and is valid.
366      * @throws ImplementationException
367      *             Almost any error.
368      */
369     private boolean setToNextScheduledValue(final GregorianCalendar cal, final int field)
370             throws ImplementationExceptionResponse {
371         int next;
372         TreeSet<Integer> vals = getValues(field);
373         if (vals.isEmpty()) {
374             next = cal.get(field) + 1;
375         } else {
376             try {
377                 // get next scheduled value which is bigger than current
378                 int incrValue = cal.get(field) + 1;
379                 next = vals.tailSet(new Integer(incrValue)).first().intValue();
380             } catch (NoSuchElementException nse) {
381                 // there is no bigger scheduled value
382                 return false;
383             }
384         }
385         if (next > cal.getActualMaximum(field) || next < cal.getActualMinimum(field)) {
386             return false;
387         }
388         // all is well, set it to next
389         cal.set(field, next);
390         return true;
391     }
392 
393     /**
394      * Sets the field of a GregorianCalender to its next valid value, but first
395      * sets all smaller fields to their minima and rolls the datefield is
396      * defined as the next possible value according to the calendar type used
397      * possibly superseded by the defined values in the schedule we have.
398      * Returns whether the new value has been set and is valid.
399      * 
400      * @param cal
401      *            Calendar to adjust.
402      * @param field
403      *            Field to adjust.<br>
404      *            TODO: smallerField wouldn't be necessary.
405      * @param smallerField
406      *            Field from where on to minimize.
407      * @return Returns whether the new value has been set and is valid.
408      * @throws ImplementationException
409      *             Almost any error.
410      */
411     private boolean setFieldToNextValidRoll(final GregorianCalendar cal, final int field, final int smallerField)
412             throws ImplementationExceptionResponse {
413         setFieldsToMinimum(cal, smallerField);
414         return setToNextScheduledValue(cal, field);
415     }
416 
417     /**
418      * Sets the field of a GregorianCalender to its minimum, which is defined as
419      * the minimal possible value according to the calendar type possibly
420      * superseded by the defined values in the schedule we have. Returns whether
421      * the new value has been set and is valid.
422      * 
423      * @param cal
424      *            Calendar to adjust.
425      * @param field
426      *            Field to adjust.
427      * @return Returns whether the new value has been set and is valid.
428      * @throws ImplementationException
429      *             Almost any error.
430      */
431     private boolean setFieldToMinimum(final GregorianCalendar cal, final int field)
432             throws ImplementationExceptionResponse {
433         int min;
434         TreeSet<Integer> values = getValues(field);
435         if (values.isEmpty()) {
436             min = cal.getActualMinimum(field);
437         } else {
438             min = Math.max(values.first().intValue(), cal.getActualMinimum(field));
439             if (min > cal.getActualMaximum(field)) {
440                 min = cal.getActualMaximum(field);
441                 if (!values.contains(Integer.valueOf(min)) || min < cal.getActualMinimum(field)
442                         || min > cal.getActualMaximum(field)) {
443                     return false;
444                 }
445             }
446         }
447         cal.set(field, min);
448         return true;
449     }
450 
451     /**
452      * Sets the given field of a GregorianCalender and all smaller fields (not
453      * WEEK_OF_DAY) to their minimum, which is defined as the minimal possible
454      * value according to the calendar type used possibly superseded by the
455      * defined values in the schedule we have. Returns whether the new values
456      * have been set and are all valid.
457      * 
458      * @param cal
459      *            The Calendar instance to adjust.
460      * @param largestField
461      *            This field and smaller ones are reset
462      * @return True if setting to min worked for all values.
463      * @throws ImplementationException
464      *             Various errors.
465      */
466     private boolean setFieldsToMinimum(final GregorianCalendar cal, final int largestField)
467             throws ImplementationExceptionResponse {
468         boolean result = true;
469         switch (largestField) {
470         case (MONTH):
471             result = setFieldToMinimum(cal, MONTH) && result;
472         case (DAY_OF_MONTH):
473             result = setFieldToMinimum(cal, DAY_OF_MONTH) && result;
474         case (HOUR_OF_DAY):
475             result = setFieldToMinimum(cal, HOUR_OF_DAY) && result;
476         case (MINUTE):
477             result = setFieldToMinimum(cal, MINUTE) && result;
478         case (SECOND):
479             result = setFieldToMinimum(cal, SECOND) && result;
480             break;
481         default:
482             String msg = "Invalid field: " + largestField;
483             ImplementationExceptionResponse iex = new ImplementationExceptionResponse(msg);
484             LOG.error(msg, iex);
485             throw iex;
486 
487         }
488         return result;
489     }
490 
491     /**
492      * Returns the values belonging to the given field of a GregorianCalendar.
493      * 
494      * @param field
495      *            The field id of a GregorianCalendar.
496      * @see GregorianCalendar
497      * @return The corresponding schedule values.
498      * @throws ImplementationException
499      *             In case of a access to an unknown field.
500      */
501     private TreeSet<Integer> getValues(final int field) throws ImplementationExceptionResponse {
502         switch (field) {
503         case (DAY_OF_WEEK):
504             return daysOfWeek;
505         case (MONTH):
506             return months;
507         case (DAY_OF_MONTH):
508             return daysOfMonth;
509         case (HOUR_OF_DAY):
510             return hours;
511         case (MINUTE):
512             return minutes;
513         case (SECOND):
514             return seconds;
515         default:
516             String msg = "Invalid field: " + field;
517             ImplementationExceptionResponse iex = new ImplementationExceptionResponse(msg);
518             LOG.error(msg, iex);
519             throw iex;
520         }
521     }
522 
523     /**
524      * Checks whether the given values, which are either numbers or ranges, are
525      * valid (parsable as Integer) and adds the value to the correct set of
526      * values (e.g. seconds).
527      * 
528      * @param values
529      *            The numbers and ranges to be checked and added.
530      * @param type
531      *            The name of the schedule element, e.g. 'second'.
532      * @param min
533      *            The minimum allowed value.
534      * @param max
535      *            The maximum allowed value.
536      * @throws SubscriptionControlsException
537      *             If one of the given values is invalid, i.e. does not lie
538      *             between the <code>min</code> and <code>max</code> value.
539      */
540     private void handleValues(final String[] values, final String type, final int min, final int max)
541             throws SubscriptionControlsExceptionResponse {
542         // we put values into this sorted set
543         TreeSet<Integer> vals = new TreeSet<Integer>();
544         for (String v : values) {
545             try {
546                 if (v.startsWith("[")) {
547                     // it's a range
548                     String[] range = v.substring(1, v.length() - 1).split("-");
549                     int start = Integer.parseInt(range[0]);
550                     int end = Integer.parseInt(range[1]);
551                     // check range
552                     if (start < min || end > max || start > end) {
553                         throw new SubscriptionControlsExceptionResponse("The value for '" + type
554                                 + "' is out of range in the query schedule.");
555                     }
556                     // add all values in the range
557                     for (int value = start; value <= end; value++) {
558                         vals = addValue(value, type, vals);
559                     }
560                 } else if (!v.equals("")) {
561                     // it's a single value
562                     int value = Integer.parseInt(v);
563                     // check value
564                     if (value < min || value > max) {
565                         throw new SubscriptionControlsExceptionResponse("The value for '" + type
566                                 + "' is out of range in the query schedule.");
567                     }
568                     // add value
569                     vals = addValue(value, type, vals);
570                 }
571             } catch (Exception e) {
572                 String msg = "The value '" + v + "' for parameter '" + type + "' is invalid in the query schedule.";
573                 LOG.info("USER ERROR: " + msg + e.getMessage());
574                 throw new SubscriptionControlsExceptionResponse(msg);
575             }
576         }
577 
578         if (type.equals("second")) {
579             this.seconds = vals;
580         } else if (type.equals("minute")) {
581             this.minutes = vals;
582         } else if (type.equals("hour")) {
583             this.hours = vals;
584         } else if (type.equals("dayOfMonth")) {
585             this.daysOfMonth = vals;
586         } else if (type.equals("month")) {
587             this.months = vals;
588         } else if (type.equals("dayOfWeek")) {
589             this.daysOfWeek = vals;
590         }
591     }
592 
593     /**
594      * Adds a schedule value to the given set of values with some special
595      * treatment for 'month' and 'dayOfWeek'.
596      * 
597      * @param value
598      *            The value to be added.
599      * @param type
600      *            The name of the schedule element, e.g. 'second'.
601      * @param vals
602      *            The set of values to which the value should be added.
603      * @return The modified set of values.
604      */
605     private TreeSet<Integer> addValue(final int value, final String type, final TreeSet<Integer> vals) {
606         if (type.equals("dayOfWeek")) {
607             vals.add(new Integer((value % 7) + 1));
608         } else if (type.equals("month")) {
609             vals.add(new Integer(value - 1));
610         } else {
611             vals.add(new Integer(value));
612         }
613         return vals;
614     }
615 
616     /**
617      * @return The days of month from this schedule.
618      */
619     public TreeSet<Integer> getDaysOfMonth() {
620         return daysOfMonth;
621     }
622 
623     /**
624      * @return The days of week from this schedule.
625      */
626     public TreeSet<Integer> getDaysOfWeek() {
627         return daysOfWeek;
628     }
629 
630     /**
631      * @return The hours from this schedule.
632      */
633     public TreeSet<Integer> getHours() {
634         return hours;
635     }
636 
637     /**
638      * @return The minutes from this schedule.
639      */
640     public TreeSet<Integer> getMinutes() {
641         return minutes;
642     }
643 
644     /**
645      * @return The months from this schedule.
646      */
647     public TreeSet<Integer> getMonths() {
648         return months;
649     }
650 
651     /**
652      * @return The seconds from this schedule.
653      */
654     public TreeSet<Integer> getSeconds() {
655         return seconds;
656     }
657 }