1
22
23
52
53 package com.liferay.util.cal;
54
55 import com.liferay.portal.kernel.util.CalendarFactoryUtil;
56 import com.liferay.portal.kernel.util.StringPool;
57
58 import java.io.Serializable;
59
60 import java.util.Calendar;
61 import java.util.Date;
62 import java.util.TimeZone;
63
64
71 public class Recurrence implements Serializable {
72
73
76 public final static int DAILY = 3;
77
78
81 public final static int WEEKLY = 4;
82
83
86 public final static int MONTHLY = 5;
87
88
91 public final static int YEARLY = 6;
92
93
96 public final static int NO_RECURRENCE = 7;
97
98
101 protected Calendar dtStart;
102
103
106 protected Duration duration;
107
108
111 protected int frequency;
112
113
116 protected int interval;
117
118
121 protected int occurrence = 0;
122
123
126 protected Calendar until;
127
128
131 protected DayAndPosition[] byDay;
132
133
136 protected int[] byMonthDay;
137
138
141 protected int[] byYearDay;
142
143
146 protected int[] byWeekNo;
147
148
151 protected int[] byMonth;
152
153
156 public Recurrence() {
157 this(null, new Duration(), NO_RECURRENCE);
158 }
159
160
163 public Recurrence(Calendar start, Duration dur) {
164 this(start, dur, NO_RECURRENCE);
165 }
166
167
170 public Recurrence(Calendar start, Duration dur, int freq) {
171 setDtStart(start);
172
173 duration = (Duration)dur.clone();
174 frequency = freq;
175 interval = 1;
176 }
177
178
179
180
185 public Calendar getDtStart() {
186 return (Calendar)dtStart.clone();
187 }
188
189
192 public void setDtStart(Calendar start) {
193 int oldStart;
194
195 if (dtStart != null) {
196 oldStart = dtStart.getFirstDayOfWeek();
197 }
198 else {
199 oldStart = Calendar.MONDAY;
200 }
201
202 if (start == null) {
203 dtStart = CalendarFactoryUtil.getCalendar(
204 TimeZone.getTimeZone(StringPool.UTC));
205
206 dtStart.setTime(new Date(0L));
207 }
208 else {
209 dtStart = (Calendar)start.clone();
210
211 dtStart.clear(Calendar.ZONE_OFFSET);
212 dtStart.clear(Calendar.DST_OFFSET);
213 dtStart.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
214 }
215
216 dtStart.setMinimalDaysInFirstWeek(4);
217 dtStart.setFirstDayOfWeek(oldStart);
218 }
219
220
225 public Duration getDuration() {
226 return (Duration)duration.clone();
227 }
228
229
232 public void setDuration(Duration d) {
233 duration = (Duration)d.clone();
234 }
235
236
241 public Calendar getDtEnd() {
242
243
247 Calendar tempEnd = (Calendar)dtStart.clone();
248
249 tempEnd.setTime(new Date(dtStart.getTime().getTime()
250 + duration.getInterval()));
251
252 return tempEnd;
253 }
254
255
258 public void setDtEnd(Calendar end) {
259 Calendar tempEnd = (Calendar)end.clone();
260
261 tempEnd.clear(Calendar.ZONE_OFFSET);
262 tempEnd.clear(Calendar.DST_OFFSET);
263 tempEnd.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
264 duration.setInterval(tempEnd.getTime().getTime()
265 - dtStart.getTime().getTime());
266 }
267
268
273 public int getFrequency() {
274 return frequency;
275 }
276
277
280 public void setFrequency(int freq) {
281 if ((frequency != DAILY) && (frequency != WEEKLY)
282 && (frequency != MONTHLY) && (frequency != YEARLY)
283 && (frequency != NO_RECURRENCE)) {
284 throw new IllegalArgumentException("Invalid frequency");
285 }
286
287 frequency = freq;
288 }
289
290
295 public int getInterval() {
296 return interval;
297 }
298
299
302 public void setInterval(int intr) {
303 interval = (intr > 0) ? intr : 1;
304 }
305
306
311 public int getOccurrence() {
312 return occurrence;
313 }
314
315
318 public void setOccurrence(int occur) {
319 occurrence = occur;
320 }
321
322
327 public Calendar getUntil() {
328 return ((until != null) ? (Calendar)until.clone() : null);
329 }
330
331
334 public void setUntil(Calendar u) {
335 if (u == null) {
336 until = null;
337
338 return;
339 }
340
341 until = (Calendar)u.clone();
342
343 until.clear(Calendar.ZONE_OFFSET);
344 until.clear(Calendar.DST_OFFSET);
345 until.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
346 }
347
348
353 public int getWeekStart() {
354 return dtStart.getFirstDayOfWeek();
355 }
356
357
360 public void setWeekStart(int weekstart) {
361 dtStart.setFirstDayOfWeek(weekstart);
362 }
363
364
369 public DayAndPosition[] getByDay() {
370 if (byDay == null) {
371 return null;
372 }
373
374 DayAndPosition[] b = new DayAndPosition[byDay.length];
375
376
380 for (int i = 0; i < byDay.length; i++) {
381 b[i] = (DayAndPosition)byDay[i].clone();
382 }
383
384 return b;
385 }
386
387
390 public void setByDay(DayAndPosition[] b) {
391 if (b == null) {
392 byDay = null;
393
394 return;
395 }
396
397 byDay = new DayAndPosition[b.length];
398
399
403 for (int i = 0; i < b.length; i++) {
404 byDay[i] = (DayAndPosition)b[i].clone();
405 }
406 }
407
408
413 public int[] getByMonthDay() {
414 if (byMonthDay == null) {
415 return null;
416 }
417
418 int[] b = new int[byMonthDay.length];
419
420 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
421
422 return b;
423 }
424
425
428 public void setByMonthDay(int[] b) {
429 if (b == null) {
430 byMonthDay = null;
431
432 return;
433 }
434
435 byMonthDay = new int[b.length];
436
437 System.arraycopy(b, 0, byMonthDay, 0, b.length);
438 }
439
440
445 public int[] getByYearDay() {
446 if (byYearDay == null) {
447 return null;
448 }
449
450 int[] b = new int[byYearDay.length];
451
452 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
453
454 return b;
455 }
456
457
460 public void setByYearDay(int[] b) {
461 if (b == null) {
462 byYearDay = null;
463
464 return;
465 }
466
467 byYearDay = new int[b.length];
468
469 System.arraycopy(b, 0, byYearDay, 0, b.length);
470 }
471
472
477 public int[] getByWeekNo() {
478 if (byWeekNo == null) {
479 return null;
480 }
481
482 int[] b = new int[byWeekNo.length];
483
484 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
485
486 return b;
487 }
488
489
492 public void setByWeekNo(int[] b) {
493 if (b == null) {
494 byWeekNo = null;
495
496 return;
497 }
498
499 byWeekNo = new int[b.length];
500
501 System.arraycopy(b, 0, byWeekNo, 0, b.length);
502 }
503
504
509 public int[] getByMonth() {
510 if (byMonth == null) {
511 return null;
512 }
513
514 int[] b = new int[byMonth.length];
515
516 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
517
518 return b;
519 }
520
521
524 public void setByMonth(int[] b) {
525 if (b == null) {
526 byMonth = null;
527
528 return;
529 }
530
531 byMonth = new int[b.length];
532
533 System.arraycopy(b, 0, byMonth, 0, b.length);
534 }
535
536
541 public boolean isInRecurrence(Calendar current) {
542 return isInRecurrence(current, false);
543 }
544
545
550 public boolean isInRecurrence(Calendar current, boolean debug) {
551 Calendar myCurrent = (Calendar)current.clone();
552
553
555 myCurrent.clear(Calendar.ZONE_OFFSET);
556 myCurrent.clear(Calendar.DST_OFFSET);
557 myCurrent.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
558 myCurrent.setMinimalDaysInFirstWeek(4);
559 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
560
561 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
562
563
565 if (debug) {
566 System.err.println("current < start");
567 }
568
569 return false;
570 }
571
572 if (myCurrent.getTime().getTime()
573 < dtStart.getTime().getTime() + duration.getInterval()) {
574
575
577 if (debug) {
578 System.err.println("within duration of start");
579 }
580
581 return true;
582 }
583
584 Calendar candidate = getCandidateStartTime(myCurrent);
585
586
587
588 while (candidate.getTime().getTime() + duration.getInterval()
589 > myCurrent.getTime().getTime()) {
590 if (candidateIsInRecurrence(candidate, debug)) {
591 return true;
592 }
593
594
595
596 candidate.add(Calendar.SECOND, -1);
597
598
599
600 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
601 if (debug) {
602 System.err.println("No candidates after dtStart");
603 }
604
605 return false;
606 }
607
608 candidate = getCandidateStartTime(candidate);
609 }
610
611 if (debug) {
612 System.err.println("No matching candidates");
613 }
614
615 return false;
616 }
617
618
623 protected boolean candidateIsInRecurrence(Calendar candidate,
624 boolean debug) {
625 if ((until != null)
626 && (candidate.getTime().getTime() > until.getTime().getTime())) {
627
628
630 if (debug) {
631 System.err.println("after until");
632 }
633
634 return false;
635 }
636
637 if (getRecurrenceCount(candidate) % interval != 0) {
638
639
641 if (debug) {
642 System.err.println("not an interval rep");
643 }
644
645 return false;
646 }
647 else if ((occurrence > 0) &&
648 (getRecurrenceCount(candidate) >= occurrence)) {
649
650 return false;
651 }
652
653 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
654 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
655 ||!matchesByMonth(candidate)) {
656
657
659 if (debug) {
660 System.err.println("doesn't match a by*");
661 }
662
663 return false;
664 }
665
666 if (debug) {
667 System.err.println("All checks succeeded");
668 }
669
670 return true;
671 }
672
673
678 protected int getMinimumInterval() {
679 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
680 || (byYearDay != null)) {
681 return DAILY;
682 }
683 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
684 return WEEKLY;
685 }
686 else if ((frequency == MONTHLY) || (byMonth != null)) {
687 return MONTHLY;
688 }
689 else if (frequency == YEARLY) {
690 return YEARLY;
691 }
692 else if (frequency == NO_RECURRENCE) {
693 return NO_RECURRENCE;
694 }
695 else {
696
697
699 throw new IllegalStateException(
700 "Internal error: Unknown frequency value");
701 }
702 }
703
704
709 public Calendar getCandidateStartTime(Calendar current) {
710 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
711 throw new IllegalArgumentException("Current time before DtStart");
712 }
713
714 int minInterval = getMinimumInterval();
715 Calendar candidate = (Calendar)current.clone();
716
717 if (true) {
718
719
721 candidate.clear(Calendar.ZONE_OFFSET);
722 candidate.clear(Calendar.DST_OFFSET);
723 candidate.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
724 candidate.setMinimalDaysInFirstWeek(4);
725 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
726 }
727
728 if (frequency == NO_RECURRENCE) {
729 candidate.setTime(dtStart.getTime());
730
731 return candidate;
732 }
733
734 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
735 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
736 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
737
738 switch (minInterval) {
739
740 case DAILY :
741
742
743
744 break;
745
746 case WEEKLY :
747 reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
748 candidate);
749 break;
750
751 case MONTHLY :
752 reduce_day_of_month(dtStart, candidate);
753 break;
754
755 case YEARLY :
756 reduce_day_of_year(dtStart, candidate);
757 break;
758 }
759
760 return candidate;
761 }
762
763
766 protected static void reduce_constant_length_field(int field,
767 Calendar start,
768 Calendar candidate) {
769 if ((start.getMaximum(field) != start.getLeastMaximum(field))
770 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
771 throw new IllegalArgumentException("Not a constant length field");
772 }
773
774 int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
775 + 1);
776 int delta = start.get(field) - candidate.get(field);
777
778 if (delta > 0) {
779 delta -= fieldLength;
780 }
781
782 candidate.add(field, delta);
783 }
784
785
788 protected static void reduce_day_of_month(Calendar start,
789 Calendar candidate) {
790 Calendar tempCal = (Calendar)candidate.clone();
791
792 tempCal.add(Calendar.MONTH, -1);
793
794 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
795
796 if (delta > 0) {
797 delta -= tempCal.getActualMaximum(Calendar.DATE);
798 }
799
800 candidate.add(Calendar.DATE, delta);
801
802 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
803 tempCal.add(Calendar.MONTH, -1);
804 candidate.add(Calendar.DATE,
805 -tempCal.getActualMaximum(Calendar.DATE));
806 }
807 }
808
809
812 protected static void reduce_day_of_year(Calendar start,
813 Calendar candidate) {
814 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
815 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
816 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
817 candidate.add(Calendar.YEAR, -1);
818 }
819
820
821
822 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
823 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
824
825 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
826 || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
827 candidate.add(Calendar.YEAR, -1);
828 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
829 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
830 }
831 }
832
833
838 protected int getRecurrenceCount(Calendar candidate) {
839 switch (frequency) {
840
841 case NO_RECURRENCE :
842 return 0;
843
844 case DAILY :
845 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
846
847 case WEEKLY :
848 Calendar tempCand = (Calendar)candidate.clone();
849
850 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
851
852 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
853
854 case MONTHLY :
855 return (int)(getMonthNumber(candidate)
856 - getMonthNumber(dtStart));
857
858 case YEARLY :
859 return candidate.get(Calendar.YEAR)
860 - dtStart.get(Calendar.YEAR);
861
862 default :
863 throw new IllegalStateException("bad frequency internally...");
864 }
865 }
866
867
872 protected static long getDayNumber(Calendar cal) {
873 Calendar tempCal = (Calendar)cal.clone();
874
875
877 tempCal.set(Calendar.MILLISECOND, 0);
878 tempCal.set(Calendar.SECOND, 0);
879 tempCal.set(Calendar.MINUTE, 0);
880 tempCal.set(Calendar.HOUR_OF_DAY, 0);
881
882 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
883 }
884
885
890 protected static long getWeekNumber(Calendar cal) {
891 Calendar tempCal = (Calendar)cal.clone();
892
893
895 tempCal.set(Calendar.MILLISECOND, 0);
896 tempCal.set(Calendar.SECOND, 0);
897 tempCal.set(Calendar.MINUTE, 0);
898 tempCal.set(Calendar.HOUR_OF_DAY, 0);
899
900
902 int delta = tempCal.getFirstDayOfWeek()
903 - tempCal.get(Calendar.DAY_OF_WEEK);
904
905 if (delta > 0) {
906 delta -= 7;
907 }
908
909
911
914 long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24
915 * 60 * 60 * 1000L;
916
917 return (tempCal.getTime().getTime() - weekEpoch)
918 / (7 * 24 * 60 * 60 * 1000);
919 }
920
921
926 protected static long getMonthNumber(Calendar cal) {
927 return (cal.get(Calendar.YEAR) - 1970) * 12
928 + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
929 }
930
931
936 protected boolean matchesByDay(Calendar candidate) {
937 if ((byDay == null) || (byDay.length == 0)) {
938
939
940
941 return true;
942 }
943
944 int i;
945
946 for (i = 0; i < byDay.length; i++) {
947 if (matchesIndividualByDay(candidate, byDay[i])) {
948 return true;
949 }
950 }
951
952 return false;
953 }
954
955
960 protected boolean matchesIndividualByDay(Calendar candidate,
961 DayAndPosition pos) {
962 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
963 return false;
964 }
965
966 int position = pos.getDayPosition();
967
968 if (position == 0) {
969 return true;
970 }
971
972 int field;
973
974 switch (frequency) {
975
976 case MONTHLY :
977 field = Calendar.DAY_OF_MONTH;
978 break;
979
980 case YEARLY :
981 field = Calendar.DAY_OF_YEAR;
982 break;
983
984 default :
985 throw new IllegalStateException(
986 "byday has a day position "
987 + "in non-MONTHLY or YEARLY recurrence");
988 }
989
990 if (position > 0) {
991 int day_of_week_in_field = ((candidate.get(field) - 1) / 7) + 1;
992
993 return (position == day_of_week_in_field);
994 }
995 else {
996
997
998
999 int negative_day_of_week_in_field =
1000 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
1001 + 1;
1002
1003 return (-position == negative_day_of_week_in_field);
1004 }
1005 }
1006
1007
1012 protected static boolean matchesByField(int[] array, int field,
1013 Calendar candidate,
1014 boolean allowNegative) {
1015 if ((array == null) || (array.length == 0)) {
1016
1017
1018
1019 return true;
1020 }
1021
1022 int i;
1023
1024 for (i = 0; i < array.length; i++) {
1025 int val;
1026
1027 if (allowNegative && (array[i] < 0)) {
1028
1029
1031 int max = candidate.getActualMaximum(field);
1032
1033 val = (max + 1) + array[i];
1034 }
1035 else {
1036 val = array[i];
1037 }
1038
1039 if (val == candidate.get(field)) {
1040 return true;
1041 }
1042 }
1043
1044 return false;
1045 }
1046
1047
1052 protected boolean matchesByMonthDay(Calendar candidate) {
1053 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1054 }
1055
1056
1061 protected boolean matchesByYearDay(Calendar candidate) {
1062 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1063 }
1064
1065
1070 protected boolean matchesByWeekNo(Calendar candidate) {
1071 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1072 }
1073
1074
1079 protected boolean matchesByMonth(Calendar candidate) {
1080 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1081 }
1082
1083
1088 public String toString() {
1089 StringBuilder sb = new StringBuilder();
1090
1091 sb.append(getClass().getName());
1092 sb.append("[dtStart=");
1093 sb.append((dtStart != null) ? dtStart.toString() : "null");
1094 sb.append(",duration=");
1095 sb.append((duration != null) ? duration.toString() : "null");
1096 sb.append(",frequency=");
1097 sb.append(frequency);
1098 sb.append(",interval=");
1099 sb.append(interval);
1100 sb.append(",until=");
1101 sb.append((until != null) ? until.toString() : "null");
1102 sb.append(",byDay=");
1103
1104 if (byDay == null) {
1105 sb.append("null");
1106 }
1107 else {
1108 sb.append("[");
1109
1110 for (int i = 0; i < byDay.length; i++) {
1111 if (i != 0) {
1112 sb.append(",");
1113 }
1114
1115 if (byDay[i] != null) {
1116 sb.append(byDay[i].toString());
1117 }
1118 else {
1119 sb.append("null");
1120 }
1121 }
1122
1123 sb.append("]");
1124 }
1125
1126 sb.append(",byMonthDay=");
1127 sb.append(stringizeIntArray(byMonthDay));
1128 sb.append(",byYearDay=");
1129 sb.append(stringizeIntArray(byYearDay));
1130 sb.append(",byWeekNo=");
1131 sb.append(stringizeIntArray(byWeekNo));
1132 sb.append(",byMonth=");
1133 sb.append(stringizeIntArray(byMonth));
1134 sb.append(']');
1135
1136 return sb.toString();
1137 }
1138
1139
1144 private String stringizeIntArray(int[] a) {
1145 if (a == null) {
1146 return "null";
1147 }
1148
1149 StringBuilder sb = new StringBuilder();
1150
1151 sb.append("[");
1152
1153 for (int i = 0; i < a.length; i++) {
1154 if (i != 0) {
1155 sb.append(",");
1156 }
1157
1158 sb.append(a[i]);
1159 }
1160
1161 sb.append("]");
1162
1163 return sb.toString();
1164 }
1165
1166}