1
22
23
52
53 package com.liferay.portal.kernel.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
69 public class Recurrence implements Serializable {
70
71
74 public final static int DAILY = 3;
75
76
79 public final static int WEEKLY = 4;
80
81
84 public final static int MONTHLY = 5;
85
86
89 public final static int YEARLY = 6;
90
91
94 public final static int NO_RECURRENCE = 7;
95
96
99 protected Calendar dtStart;
100
101
104 protected Duration duration;
105
106
109 protected int frequency;
110
111
114 protected int interval;
115
116
119 protected int occurrence = 0;
120
121
124 protected Calendar until;
125
126
129 protected DayAndPosition[] byDay;
130
131
134 protected int[] byMonthDay;
135
136
139 protected int[] byYearDay;
140
141
144 protected int[] byWeekNo;
145
146
149 protected int[] byMonth;
150
151
154 public Recurrence() {
155 this(null, new Duration(), NO_RECURRENCE);
156 }
157
158
161 public Recurrence(Calendar start, Duration dur) {
162 this(start, dur, NO_RECURRENCE);
163 }
164
165
168 public Recurrence(Calendar start, Duration dur, int freq) {
169 setDtStart(start);
170
171 duration = (Duration)dur.clone();
172 frequency = freq;
173 interval = 1;
174 }
175
176
177
178
183 public Calendar getDtStart() {
184 return (Calendar)dtStart.clone();
185 }
186
187
190 public void setDtStart(Calendar start) {
191 int oldStart;
192
193 if (dtStart != null) {
194 oldStart = dtStart.getFirstDayOfWeek();
195 }
196 else {
197 oldStart = Calendar.MONDAY;
198 }
199
200 if (start == null) {
201 dtStart = CalendarFactoryUtil.getCalendar(
202 TimeZone.getTimeZone(StringPool.UTC));
203
204 dtStart.setTime(new Date(0L));
205 }
206 else {
207 dtStart = (Calendar)start.clone();
208
209 dtStart.clear(Calendar.ZONE_OFFSET);
210 dtStart.clear(Calendar.DST_OFFSET);
211 dtStart.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
212 }
213
214 dtStart.setMinimalDaysInFirstWeek(4);
215 dtStart.setFirstDayOfWeek(oldStart);
216 }
217
218
223 public Duration getDuration() {
224 return (Duration)duration.clone();
225 }
226
227
230 public void setDuration(Duration d) {
231 duration = (Duration)d.clone();
232 }
233
234
239 public Calendar getDtEnd() {
240
241
245 Calendar tempEnd = (Calendar)dtStart.clone();
246
247 tempEnd.setTime(new Date(dtStart.getTime().getTime()
248 + duration.getInterval()));
249
250 return tempEnd;
251 }
252
253
256 public void setDtEnd(Calendar end) {
257 Calendar tempEnd = (Calendar)end.clone();
258
259 tempEnd.clear(Calendar.ZONE_OFFSET);
260 tempEnd.clear(Calendar.DST_OFFSET);
261 tempEnd.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
262 duration.setInterval(tempEnd.getTime().getTime()
263 - dtStart.getTime().getTime());
264 }
265
266
271 public int getFrequency() {
272 return frequency;
273 }
274
275
278 public void setFrequency(int freq) {
279 if ((frequency != DAILY) && (frequency != WEEKLY)
280 && (frequency != MONTHLY) && (frequency != YEARLY)
281 && (frequency != NO_RECURRENCE)) {
282 throw new IllegalArgumentException("Invalid frequency");
283 }
284
285 frequency = freq;
286 }
287
288
293 public int getInterval() {
294 return interval;
295 }
296
297
300 public void setInterval(int intr) {
301 interval = (intr > 0) ? intr : 1;
302 }
303
304
309 public int getOccurrence() {
310 return occurrence;
311 }
312
313
316 public void setOccurrence(int occur) {
317 occurrence = occur;
318 }
319
320
325 public Calendar getUntil() {
326 return ((until != null) ? (Calendar)until.clone() : null);
327 }
328
329
332 public void setUntil(Calendar u) {
333 if (u == null) {
334 until = null;
335
336 return;
337 }
338
339 until = (Calendar)u.clone();
340
341 until.clear(Calendar.ZONE_OFFSET);
342 until.clear(Calendar.DST_OFFSET);
343 until.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
344 }
345
346
351 public int getWeekStart() {
352 return dtStart.getFirstDayOfWeek();
353 }
354
355
358 public void setWeekStart(int weekstart) {
359 dtStart.setFirstDayOfWeek(weekstart);
360 }
361
362
367 public DayAndPosition[] getByDay() {
368 if (byDay == null) {
369 return null;
370 }
371
372 DayAndPosition[] b = new DayAndPosition[byDay.length];
373
374
378 for (int i = 0; i < byDay.length; i++) {
379 b[i] = (DayAndPosition)byDay[i].clone();
380 }
381
382 return b;
383 }
384
385
388 public void setByDay(DayAndPosition[] b) {
389 if (b == null) {
390 byDay = null;
391
392 return;
393 }
394
395 byDay = new DayAndPosition[b.length];
396
397
401 for (int i = 0; i < b.length; i++) {
402 byDay[i] = (DayAndPosition)b[i].clone();
403 }
404 }
405
406
411 public int[] getByMonthDay() {
412 if (byMonthDay == null) {
413 return null;
414 }
415
416 int[] b = new int[byMonthDay.length];
417
418 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
419
420 return b;
421 }
422
423
426 public void setByMonthDay(int[] b) {
427 if (b == null) {
428 byMonthDay = null;
429
430 return;
431 }
432
433 byMonthDay = new int[b.length];
434
435 System.arraycopy(b, 0, byMonthDay, 0, b.length);
436 }
437
438
443 public int[] getByYearDay() {
444 if (byYearDay == null) {
445 return null;
446 }
447
448 int[] b = new int[byYearDay.length];
449
450 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
451
452 return b;
453 }
454
455
458 public void setByYearDay(int[] b) {
459 if (b == null) {
460 byYearDay = null;
461
462 return;
463 }
464
465 byYearDay = new int[b.length];
466
467 System.arraycopy(b, 0, byYearDay, 0, b.length);
468 }
469
470
475 public int[] getByWeekNo() {
476 if (byWeekNo == null) {
477 return null;
478 }
479
480 int[] b = new int[byWeekNo.length];
481
482 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
483
484 return b;
485 }
486
487
490 public void setByWeekNo(int[] b) {
491 if (b == null) {
492 byWeekNo = null;
493
494 return;
495 }
496
497 byWeekNo = new int[b.length];
498
499 System.arraycopy(b, 0, byWeekNo, 0, b.length);
500 }
501
502
507 public int[] getByMonth() {
508 if (byMonth == null) {
509 return null;
510 }
511
512 int[] b = new int[byMonth.length];
513
514 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
515
516 return b;
517 }
518
519
522 public void setByMonth(int[] b) {
523 if (b == null) {
524 byMonth = null;
525
526 return;
527 }
528
529 byMonth = new int[b.length];
530
531 System.arraycopy(b, 0, byMonth, 0, b.length);
532 }
533
534
539 public boolean isInRecurrence(Calendar current) {
540 return isInRecurrence(current, false);
541 }
542
543
548 public boolean isInRecurrence(Calendar current, boolean debug) {
549 Calendar myCurrent = (Calendar)current.clone();
550
551
553 myCurrent.clear(Calendar.ZONE_OFFSET);
554 myCurrent.clear(Calendar.DST_OFFSET);
555 myCurrent.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
556 myCurrent.setMinimalDaysInFirstWeek(4);
557 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
558
559 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
560
561
563 if (debug) {
564 System.err.println("current < start");
565 }
566
567 return false;
568 }
569
570 if (myCurrent.getTime().getTime()
571 < dtStart.getTime().getTime() + duration.getInterval()) {
572
573
575 if (debug) {
576 System.err.println("within duration of start");
577 }
578
579 return true;
580 }
581
582 Calendar candidate = getCandidateStartTime(myCurrent);
583
584
585
586 while (candidate.getTime().getTime() + duration.getInterval()
587 > myCurrent.getTime().getTime()) {
588 if (candidateIsInRecurrence(candidate, debug)) {
589 return true;
590 }
591
592
593
594 candidate.add(Calendar.SECOND, -1);
595
596
597
598 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
599 if (debug) {
600 System.err.println("No candidates after dtStart");
601 }
602
603 return false;
604 }
605
606 candidate = getCandidateStartTime(candidate);
607 }
608
609 if (debug) {
610 System.err.println("No matching candidates");
611 }
612
613 return false;
614 }
615
616
621 protected boolean candidateIsInRecurrence(Calendar candidate,
622 boolean debug) {
623 if ((until != null)
624 && (candidate.getTime().getTime() > until.getTime().getTime())) {
625
626
628 if (debug) {
629 System.err.println("after until");
630 }
631
632 return false;
633 }
634
635 if (getRecurrenceCount(candidate) % interval != 0) {
636
637
639 if (debug) {
640 System.err.println("not an interval rep");
641 }
642
643 return false;
644 }
645 else if ((occurrence > 0) &&
646 (getRecurrenceCount(candidate) >= occurrence)) {
647
648 return false;
649 }
650
651 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
652 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
653 ||!matchesByMonth(candidate)) {
654
655
657 if (debug) {
658 System.err.println("doesn't match a by*");
659 }
660
661 return false;
662 }
663
664 if (debug) {
665 System.err.println("All checks succeeded");
666 }
667
668 return true;
669 }
670
671
676 protected int getMinimumInterval() {
677 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
678 || (byYearDay != null)) {
679 return DAILY;
680 }
681 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
682 return WEEKLY;
683 }
684 else if ((frequency == MONTHLY) || (byMonth != null)) {
685 return MONTHLY;
686 }
687 else if (frequency == YEARLY) {
688 return YEARLY;
689 }
690 else if (frequency == NO_RECURRENCE) {
691 return NO_RECURRENCE;
692 }
693 else {
694
695
697 throw new IllegalStateException(
698 "Internal error: Unknown frequency value");
699 }
700 }
701
702
707 public Calendar getCandidateStartTime(Calendar current) {
708 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
709 throw new IllegalArgumentException("Current time before DtStart");
710 }
711
712 int minInterval = getMinimumInterval();
713 Calendar candidate = (Calendar)current.clone();
714
715 if (true) {
716
717
719 candidate.clear(Calendar.ZONE_OFFSET);
720 candidate.clear(Calendar.DST_OFFSET);
721 candidate.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
722 candidate.setMinimalDaysInFirstWeek(4);
723 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
724 }
725
726 if (frequency == NO_RECURRENCE) {
727 candidate.setTime(dtStart.getTime());
728
729 return candidate;
730 }
731
732 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
733 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
734 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
735
736 switch (minInterval) {
737
738 case DAILY :
739
740
741
742 break;
743
744 case WEEKLY :
745 reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
746 candidate);
747 break;
748
749 case MONTHLY :
750 reduce_day_of_month(dtStart, candidate);
751 break;
752
753 case YEARLY :
754 reduce_day_of_year(dtStart, candidate);
755 break;
756 }
757
758 return candidate;
759 }
760
761
764 protected static void reduce_constant_length_field(int field,
765 Calendar start,
766 Calendar candidate) {
767 if ((start.getMaximum(field) != start.getLeastMaximum(field))
768 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
769 throw new IllegalArgumentException("Not a constant length field");
770 }
771
772 int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
773 + 1);
774 int delta = start.get(field) - candidate.get(field);
775
776 if (delta > 0) {
777 delta -= fieldLength;
778 }
779
780 candidate.add(field, delta);
781 }
782
783
786 protected static void reduce_day_of_month(Calendar start,
787 Calendar candidate) {
788 Calendar tempCal = (Calendar)candidate.clone();
789
790 tempCal.add(Calendar.MONTH, -1);
791
792 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
793
794 if (delta > 0) {
795 delta -= tempCal.getActualMaximum(Calendar.DATE);
796 }
797
798 candidate.add(Calendar.DATE, delta);
799
800 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
801 tempCal.add(Calendar.MONTH, -1);
802 candidate.add(Calendar.DATE,
803 -tempCal.getActualMaximum(Calendar.DATE));
804 }
805 }
806
807
810 protected static void reduce_day_of_year(Calendar start,
811 Calendar candidate) {
812 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
813 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
814 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
815 candidate.add(Calendar.YEAR, -1);
816 }
817
818
819
820 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
821 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
822
823 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
824 || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
825 candidate.add(Calendar.YEAR, -1);
826 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
827 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
828 }
829 }
830
831
836 protected int getRecurrenceCount(Calendar candidate) {
837 switch (frequency) {
838
839 case NO_RECURRENCE :
840 return 0;
841
842 case DAILY :
843 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
844
845 case WEEKLY :
846 Calendar tempCand = (Calendar)candidate.clone();
847
848 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
849
850 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
851
852 case MONTHLY :
853 return (int)(getMonthNumber(candidate)
854 - getMonthNumber(dtStart));
855
856 case YEARLY :
857 return candidate.get(Calendar.YEAR)
858 - dtStart.get(Calendar.YEAR);
859
860 default :
861 throw new IllegalStateException("bad frequency internally...");
862 }
863 }
864
865
870 protected static long getDayNumber(Calendar cal) {
871 Calendar tempCal = (Calendar)cal.clone();
872
873
875 tempCal.set(Calendar.MILLISECOND, 0);
876 tempCal.set(Calendar.SECOND, 0);
877 tempCal.set(Calendar.MINUTE, 0);
878 tempCal.set(Calendar.HOUR_OF_DAY, 0);
879
880 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
881 }
882
883
888 protected static long getWeekNumber(Calendar cal) {
889 Calendar tempCal = (Calendar)cal.clone();
890
891
893 tempCal.set(Calendar.MILLISECOND, 0);
894 tempCal.set(Calendar.SECOND, 0);
895 tempCal.set(Calendar.MINUTE, 0);
896 tempCal.set(Calendar.HOUR_OF_DAY, 0);
897
898
900 int delta = tempCal.getFirstDayOfWeek()
901 - tempCal.get(Calendar.DAY_OF_WEEK);
902
903 if (delta > 0) {
904 delta -= 7;
905 }
906
907
909
912 long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24
913 * 60 * 60 * 1000L;
914
915 return (tempCal.getTime().getTime() - weekEpoch)
916 / (7 * 24 * 60 * 60 * 1000);
917 }
918
919
924 protected static long getMonthNumber(Calendar cal) {
925 return (cal.get(Calendar.YEAR) - 1970) * 12
926 + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
927 }
928
929
934 protected boolean matchesByDay(Calendar candidate) {
935 if ((byDay == null) || (byDay.length == 0)) {
936
937
938
939 return true;
940 }
941
942 int i;
943
944 for (i = 0; i < byDay.length; i++) {
945 if (matchesIndividualByDay(candidate, byDay[i])) {
946 return true;
947 }
948 }
949
950 return false;
951 }
952
953
958 protected boolean matchesIndividualByDay(Calendar candidate,
959 DayAndPosition pos) {
960 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
961 return false;
962 }
963
964 int position = pos.getDayPosition();
965
966 if (position == 0) {
967 return true;
968 }
969
970 int field;
971
972 switch (frequency) {
973
974 case MONTHLY :
975 field = Calendar.DAY_OF_MONTH;
976 break;
977
978 case YEARLY :
979 field = Calendar.DAY_OF_YEAR;
980 break;
981
982 default :
983 throw new IllegalStateException(
984 "byday has a day position "
985 + "in non-MONTHLY or YEARLY recurrence");
986 }
987
988 if (position > 0) {
989 int day_of_week_in_field = ((candidate.get(field) - 1) / 7) + 1;
990
991 return (position == day_of_week_in_field);
992 }
993 else {
994
995
996
997 int negative_day_of_week_in_field =
998 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
999 + 1;
1000
1001 return (-position == negative_day_of_week_in_field);
1002 }
1003 }
1004
1005
1010 protected static boolean matchesByField(int[] array, int field,
1011 Calendar candidate,
1012 boolean allowNegative) {
1013 if ((array == null) || (array.length == 0)) {
1014
1015
1016
1017 return true;
1018 }
1019
1020 int i;
1021
1022 for (i = 0; i < array.length; i++) {
1023 int val;
1024
1025 if (allowNegative && (array[i] < 0)) {
1026
1027
1029 int max = candidate.getActualMaximum(field);
1030
1031 val = (max + 1) + array[i];
1032 }
1033 else {
1034 val = array[i];
1035 }
1036
1037 if (val == candidate.get(field)) {
1038 return true;
1039 }
1040 }
1041
1042 return false;
1043 }
1044
1045
1050 protected boolean matchesByMonthDay(Calendar candidate) {
1051 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1052 }
1053
1054
1059 protected boolean matchesByYearDay(Calendar candidate) {
1060 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1061 }
1062
1063
1068 protected boolean matchesByWeekNo(Calendar candidate) {
1069 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1070 }
1071
1072
1077 protected boolean matchesByMonth(Calendar candidate) {
1078 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1079 }
1080
1081
1086 public String toString() {
1087 StringBuilder sb = new StringBuilder();
1088
1089 sb.append(getClass().getName());
1090 sb.append("[dtStart=");
1091 sb.append((dtStart != null) ? dtStart.toString() : "null");
1092 sb.append(",duration=");
1093 sb.append((duration != null) ? duration.toString() : "null");
1094 sb.append(",frequency=");
1095 sb.append(frequency);
1096 sb.append(",interval=");
1097 sb.append(interval);
1098 sb.append(",until=");
1099 sb.append((until != null) ? until.toString() : "null");
1100 sb.append(",byDay=");
1101
1102 if (byDay == null) {
1103 sb.append("null");
1104 }
1105 else {
1106 sb.append("[");
1107
1108 for (int i = 0; i < byDay.length; i++) {
1109 if (i != 0) {
1110 sb.append(",");
1111 }
1112
1113 if (byDay[i] != null) {
1114 sb.append(byDay[i].toString());
1115 }
1116 else {
1117 sb.append("null");
1118 }
1119 }
1120
1121 sb.append("]");
1122 }
1123
1124 sb.append(",byMonthDay=");
1125 sb.append(stringizeIntArray(byMonthDay));
1126 sb.append(",byYearDay=");
1127 sb.append(stringizeIntArray(byYearDay));
1128 sb.append(",byWeekNo=");
1129 sb.append(stringizeIntArray(byWeekNo));
1130 sb.append(",byMonth=");
1131 sb.append(stringizeIntArray(byMonth));
1132 sb.append(']');
1133
1134 return sb.toString();
1135 }
1136
1137
1142 private String stringizeIntArray(int[] a) {
1143 if (a == null) {
1144 return "null";
1145 }
1146
1147 StringBuilder sb = new StringBuilder();
1148
1149 sb.append("[");
1150
1151 for (int i = 0; i < a.length; i++) {
1152 if (i != 0) {
1153 sb.append(",");
1154 }
1155
1156 sb.append(a[i]);
1157 }
1158
1159 sb.append("]");
1160
1161 return sb.toString();
1162 }
1163
1164}