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 }