001
014
015
044
045 package com.liferay.portal.kernel.cal;
046
047 import com.liferay.portal.kernel.util.ArrayUtil;
048 import com.liferay.portal.kernel.util.CalendarFactoryUtil;
049 import com.liferay.portal.kernel.util.StringBundler;
050 import com.liferay.portal.kernel.util.StringPool;
051 import com.liferay.portal.kernel.util.TimeZoneUtil;
052
053 import java.io.Serializable;
054
055 import java.util.Calendar;
056 import java.util.Date;
057
058
061 public class Recurrence implements Serializable {
062
063
066 public static final int DAILY = 3;
067
068
071 public static final int MONTHLY = 5;
072
073
076 public static final int NO_RECURRENCE = 7;
077
078
081 public static final int WEEKLY = 4;
082
083
086 public static final int YEARLY = 6;
087
088
091 public Recurrence() {
092 this(null, new Duration(), NO_RECURRENCE);
093 }
094
095
098 public Recurrence(Calendar start, Duration dur) {
099 this(start, dur, NO_RECURRENCE);
100 }
101
102
105 public Recurrence(Calendar start, Duration dur, int freq) {
106 setDtStart(start);
107
108 duration = (Duration)dur.clone();
109 frequency = freq;
110 interval = 1;
111 }
112
113
114
115
120 public DayAndPosition[] getByDay() {
121 if (byDay == null) {
122 return null;
123 }
124
125 DayAndPosition[] b = new DayAndPosition[byDay.length];
126
127
131 for (int i = 0; i < byDay.length; i++) {
132 b[i] = (DayAndPosition)byDay[i].clone();
133 }
134
135 return b;
136 }
137
138
143 public int[] getByMonth() {
144 if (byMonth == null) {
145 return null;
146 }
147
148 int[] b = new int[byMonth.length];
149
150 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
151
152 return b;
153 }
154
155
160 public int[] getByMonthDay() {
161 if (byMonthDay == null) {
162 return null;
163 }
164
165 int[] b = new int[byMonthDay.length];
166
167 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
168
169 return b;
170 }
171
172
177 public int[] getByWeekNo() {
178 if (byWeekNo == null) {
179 return null;
180 }
181
182 int[] b = new int[byWeekNo.length];
183
184 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
185
186 return b;
187 }
188
189
194 public int[] getByYearDay() {
195 if (byYearDay == null) {
196 return null;
197 }
198
199 int[] b = new int[byYearDay.length];
200
201 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
202
203 return b;
204 }
205
206
212 public Calendar getCandidateStartTime(Calendar current) {
213 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
214 throw new IllegalArgumentException("Current time before DtStart");
215 }
216
217 int minInterval = getMinimumInterval();
218 Calendar candidate = (Calendar)current.clone();
219
220 if (true) {
221
222
223
224 candidate.clear(Calendar.ZONE_OFFSET);
225 candidate.clear(Calendar.DST_OFFSET);
226 candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
227 candidate.setMinimalDaysInFirstWeek(4);
228 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
229 }
230
231 if (frequency == NO_RECURRENCE) {
232 candidate.setTime(dtStart.getTime());
233
234 return candidate;
235 }
236
237 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
238 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
239 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
240
241 switch (minInterval) {
242
243 case DAILY :
244
245
246
247 break;
248
249 case WEEKLY :
250 reduce_constant_length_field(
251 Calendar.DAY_OF_WEEK, dtStart, candidate);
252 break;
253
254 case MONTHLY :
255 reduce_day_of_month(dtStart, candidate);
256 break;
257
258 case YEARLY :
259 reduce_day_of_year(dtStart, candidate);
260 break;
261 }
262
263 return candidate;
264 }
265
266
271 public Calendar getDtEnd() {
272
273
277 Calendar tempEnd = (Calendar)dtStart.clone();
278
279 tempEnd.setTime(
280 new Date(dtStart.getTime().getTime() + duration.getInterval()));
281
282 return tempEnd;
283 }
284
285
290 public Calendar getDtStart() {
291 return (Calendar)dtStart.clone();
292 }
293
294
299 public Duration getDuration() {
300 return (Duration)duration.clone();
301 }
302
303
308 public int getFrequency() {
309 return frequency;
310 }
311
312
317 public int getInterval() {
318 return interval;
319 }
320
321
326 public int getOccurrence() {
327 return occurrence;
328 }
329
330
335 public Calendar getUntil() {
336 return ((until != null) ? (Calendar)until.clone() : null);
337 }
338
339
344 public int getWeekStart() {
345 return dtStart.getFirstDayOfWeek();
346 }
347
348
354 public boolean isInRecurrence(Calendar current) {
355 return isInRecurrence(current, false);
356 }
357
358
365 public boolean isInRecurrence(Calendar current, boolean debug) {
366 Calendar myCurrent = (Calendar)current.clone();
367
368
369
370 myCurrent.clear(Calendar.ZONE_OFFSET);
371 myCurrent.clear(Calendar.DST_OFFSET);
372 myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
373 myCurrent.setMinimalDaysInFirstWeek(4);
374 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
375 myCurrent.set(Calendar.SECOND, 0);
376 myCurrent.set(Calendar.MILLISECOND, 0);
377
378 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
379
380
381
382 if (debug) {
383 System.err.println("current < start");
384 }
385
386 return false;
387 }
388
389 Calendar candidate = getCandidateStartTime(myCurrent);
390
391
392
393 while ((candidate.getTime().getTime() + duration.getInterval()) >
394 myCurrent.getTime().getTime()) {
395
396 if (candidateIsInRecurrence(candidate, debug)) {
397 return true;
398 }
399
400
401
402 candidate.add(Calendar.SECOND, -1);
403
404
405
406 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
407 if (debug) {
408 System.err.println("No candidates after dtStart");
409 }
410
411 return false;
412 }
413
414 candidate = getCandidateStartTime(candidate);
415 }
416
417 if (debug) {
418 System.err.println("No matching candidates");
419 }
420
421 return false;
422 }
423
424
427 public void setByDay(DayAndPosition[] b) {
428 if (b == null) {
429 byDay = null;
430
431 return;
432 }
433
434 byDay = new DayAndPosition[b.length];
435
436
440 for (int i = 0; i < b.length; i++) {
441 byDay[i] = (DayAndPosition)b[i].clone();
442 }
443 }
444
445
448 public void setByMonth(int[] b) {
449 if (b == null) {
450 byMonth = null;
451
452 return;
453 }
454
455 byMonth = new int[b.length];
456
457 System.arraycopy(b, 0, byMonth, 0, b.length);
458 }
459
460
463 public void setByMonthDay(int[] b) {
464 if (b == null) {
465 byMonthDay = null;
466
467 return;
468 }
469
470 byMonthDay = new int[b.length];
471
472 System.arraycopy(b, 0, byMonthDay, 0, b.length);
473 }
474
475
478 public void setByWeekNo(int[] b) {
479 if (b == null) {
480 byWeekNo = null;
481
482 return;
483 }
484
485 byWeekNo = new int[b.length];
486
487 System.arraycopy(b, 0, byWeekNo, 0, b.length);
488 }
489
490
493 public void setByYearDay(int[] b) {
494 if (b == null) {
495 byYearDay = null;
496
497 return;
498 }
499
500 byYearDay = new int[b.length];
501
502 System.arraycopy(b, 0, byYearDay, 0, b.length);
503 }
504
505
508 public void setDtEnd(Calendar end) {
509 Calendar tempEnd = (Calendar)end.clone();
510
511 tempEnd.clear(Calendar.ZONE_OFFSET);
512 tempEnd.clear(Calendar.DST_OFFSET);
513 tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
514 duration.setInterval(
515 tempEnd.getTime().getTime() - dtStart.getTime().getTime());
516 }
517
518
521 public void setDtStart(Calendar start) {
522 int oldStart;
523
524 if (dtStart != null) {
525 oldStart = dtStart.getFirstDayOfWeek();
526 }
527 else {
528 oldStart = Calendar.MONDAY;
529 }
530
531 if (start == null) {
532 dtStart = CalendarFactoryUtil.getCalendar(
533 TimeZoneUtil.getTimeZone(StringPool.UTC));
534
535 dtStart.setTime(new Date(0L));
536 }
537 else {
538 dtStart = (Calendar)start.clone();
539
540 dtStart.clear(Calendar.ZONE_OFFSET);
541 dtStart.clear(Calendar.DST_OFFSET);
542 dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
543 }
544
545 dtStart.setMinimalDaysInFirstWeek(4);
546 dtStart.setFirstDayOfWeek(oldStart);
547 dtStart.set(Calendar.SECOND, 0);
548 dtStart.set(Calendar.MILLISECOND, 0);
549 }
550
551
554 public void setDuration(Duration d) {
555 duration = (Duration)d.clone();
556 }
557
558
561 public void setFrequency(int freq) {
562 if ((frequency != DAILY) && (frequency != WEEKLY) &&
563 (frequency != MONTHLY) && (frequency != YEARLY) &&
564 (frequency != NO_RECURRENCE)) {
565
566 throw new IllegalArgumentException("Invalid frequency");
567 }
568
569 frequency = freq;
570 }
571
572
575 public void setInterval(int intr) {
576 interval = (intr > 0) ? intr : 1;
577 }
578
579
582 public void setOccurrence(int occur) {
583 occurrence = occur;
584 }
585
586
589 public void setUntil(Calendar u) {
590 if (u == null) {
591 until = null;
592
593 return;
594 }
595
596 until = (Calendar)u.clone();
597
598 until.clear(Calendar.ZONE_OFFSET);
599 until.clear(Calendar.DST_OFFSET);
600 until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
601 }
602
603
606 public void setWeekStart(int weekstart) {
607 dtStart.setFirstDayOfWeek(weekstart);
608 }
609
610
615 @Override
616 public String toString() {
617 StringBundler sb = new StringBundler();
618
619 sb.append(getClass().getName());
620 sb.append("[dtStart=");
621 sb.append((dtStart != null) ? dtStart.toString() : "null");
622 sb.append(",duration=");
623 sb.append((duration != null) ? duration.toString() : "null");
624 sb.append(",frequency=");
625 sb.append(frequency);
626 sb.append(",interval=");
627 sb.append(interval);
628 sb.append(",until=");
629 sb.append((until != null) ? until.toString() : "null");
630 sb.append(",byDay=");
631
632 if (byDay == null) {
633 sb.append("null");
634 }
635 else {
636 sb.append("[");
637
638 for (int i = 0; i < byDay.length; i++) {
639 if (i != 0) {
640 sb.append(",");
641 }
642
643 if (byDay[i] != null) {
644 sb.append(byDay[i].toString());
645 }
646 else {
647 sb.append("null");
648 }
649 }
650
651 sb.append("]");
652 }
653
654 sb.append(",byMonthDay=");
655 sb.append(stringizeIntArray(byMonthDay));
656 sb.append(",byYearDay=");
657 sb.append(stringizeIntArray(byYearDay));
658 sb.append(",byWeekNo=");
659 sb.append(stringizeIntArray(byWeekNo));
660 sb.append(",byMonth=");
661 sb.append(stringizeIntArray(byMonth));
662 sb.append(']');
663
664 return sb.toString();
665 }
666
667
672 protected static long getDayNumber(Calendar cal) {
673 Calendar tempCal = (Calendar)cal.clone();
674
675
676
677 tempCal.set(Calendar.MILLISECOND, 0);
678 tempCal.set(Calendar.SECOND, 0);
679 tempCal.set(Calendar.MINUTE, 0);
680 tempCal.set(Calendar.HOUR_OF_DAY, 0);
681
682 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
683 }
684
685
690 protected static long getMonthNumber(Calendar cal) {
691 return
692 ((cal.get(Calendar.YEAR) - 1970) * 12L) +
693 ((cal.get(Calendar.MONTH) - Calendar.JANUARY));
694 }
695
696
701 protected static long getWeekNumber(Calendar cal) {
702 Calendar tempCal = (Calendar)cal.clone();
703
704
705
706 tempCal.set(Calendar.MILLISECOND, 0);
707 tempCal.set(Calendar.SECOND, 0);
708 tempCal.set(Calendar.MINUTE, 0);
709 tempCal.set(Calendar.HOUR_OF_DAY, 0);
710
711
712
713 int delta =
714 tempCal.getFirstDayOfWeek() - tempCal.get(Calendar.DAY_OF_WEEK);
715
716 if (delta > 0) {
717 delta -= 7;
718 }
719
720
721
722
723
724
725 long weekEpoch =
726 (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L * 60 * 60 *
727 1000;
728
729 return
730 (tempCal.getTime().getTime() - weekEpoch) /
731 (7 * 24 * 60 * 60 * 1000);
732 }
733
734
737 protected static void reduce_constant_length_field(
738 int field, Calendar start, Calendar candidate) {
739
740 if ((start.getMaximum(field) != start.getLeastMaximum(field)) ||
741 (start.getMinimum(field) != start.getGreatestMinimum(field))) {
742
743 throw new IllegalArgumentException("Not a constant length field");
744 }
745
746 int fieldLength =
747 (start.getMaximum(field) - start.getMinimum(field) + 1);
748 int delta = start.get(field) - candidate.get(field);
749
750 if (delta > 0) {
751 delta -= fieldLength;
752 }
753
754 candidate.add(field, delta);
755 }
756
757
760 protected static void reduce_day_of_month(
761 Calendar start, Calendar candidate) {
762
763 Calendar tempCal = (Calendar)candidate.clone();
764
765 tempCal.add(Calendar.MONTH, -1);
766
767 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
768
769 if (delta > 0) {
770 delta -= tempCal.getActualMaximum(Calendar.DATE);
771 }
772
773 candidate.add(Calendar.DATE, delta);
774
775 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
776 tempCal.add(Calendar.MONTH, -1);
777 candidate.add(
778 Calendar.DATE, -tempCal.getActualMaximum(Calendar.DATE));
779 }
780 }
781
782
785 protected static void reduce_day_of_year(
786 Calendar start, Calendar candidate) {
787
788 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH)) ||
789 ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH)) &&
790 (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
791
792 candidate.add(Calendar.YEAR, -1);
793 }
794
795
796
797 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
798 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
799
800 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH)) ||
801 (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
802
803 candidate.add(Calendar.YEAR, -1);
804 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
805 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
806 }
807 }
808
809
814 protected boolean candidateIsInRecurrence(
815 Calendar candidate, boolean debug) {
816
817 if ((until != null) &&
818 (candidate.getTime().getTime() > until.getTime().getTime())) {
819
820
821
822 if (debug) {
823 System.err.println("after until");
824 }
825
826 return false;
827 }
828
829 if ((getRecurrenceCount(candidate) % interval) != 0) {
830
831
832
833 if (debug) {
834 System.err.println("not an interval rep");
835 }
836
837 return false;
838 }
839 else if ((occurrence > 0) &&
840 (getRecurrenceCount(candidate) >= occurrence)) {
841
842 return false;
843 }
844
845 if (!matchesByDay(candidate) || !matchesByMonthDay(candidate) ||
846 !matchesByYearDay(candidate) || !matchesByWeekNo(candidate) ||
847 !matchesByMonth(candidate)) {
848
849
850
851 if (debug) {
852 System.err.println("doesn't match a by*");
853 }
854
855 return false;
856 }
857
858 if (debug) {
859 System.err.println("All checks succeeded");
860 }
861
862 return true;
863 }
864
865
870 protected int getMinimumInterval() {
871 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null) ||
872 (byYearDay != null)) {
873
874 return DAILY;
875 }
876 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
877 return WEEKLY;
878 }
879 else if ((frequency == MONTHLY) || (byMonth != null)) {
880 return MONTHLY;
881 }
882 else if (frequency == YEARLY) {
883 return YEARLY;
884 }
885 else if (frequency == NO_RECURRENCE) {
886 return NO_RECURRENCE;
887 }
888 else {
889
890
891
892 throw new IllegalStateException(
893 "Internal error: Unknown frequency value");
894 }
895 }
896
897
902 protected int getRecurrenceCount(Calendar candidate) {
903 switch (frequency) {
904
905 case NO_RECURRENCE :
906 return 0;
907
908 case DAILY :
909 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
910
911 case WEEKLY :
912 Calendar tempCand = (Calendar)candidate.clone();
913
914 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
915
916 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
917
918 case MONTHLY :
919 return
920 (int)(getMonthNumber(candidate) - getMonthNumber(dtStart));
921
922 case YEARLY :
923 return
924 candidate.get(Calendar.YEAR) - dtStart.get(Calendar.YEAR);
925
926 default :
927 throw new IllegalStateException("bad frequency internally...");
928 }
929 }
930
931
936 protected boolean matchesByDay(Calendar candidate) {
937 if (ArrayUtil.isEmpty(byDay)) {
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 matchesByField(
961 int[] array, int field, Calendar candidate, boolean allowNegative) {
962
963 if (ArrayUtil.isEmpty(array)) {
964
965
966
967 return true;
968 }
969
970 int i;
971
972 for (i = 0; i < array.length; i++) {
973 int val;
974
975 if (allowNegative && (array[i] < 0)) {
976
977
978
979 int max = candidate.getActualMaximum(field);
980
981 val = (max + 1) + array[i];
982 }
983 else {
984 val = array[i];
985 }
986
987 if (val == candidate.get(field)) {
988 return true;
989 }
990 }
991
992 return false;
993 }
994
995
1000 protected boolean matchesByMonth(Calendar candidate) {
1001 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1002 }
1003
1004
1009 protected boolean matchesByMonthDay(Calendar candidate) {
1010 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1011 }
1012
1013
1018 protected boolean matchesByWeekNo(Calendar candidate) {
1019 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1020 }
1021
1022
1027 protected boolean matchesByYearDay(Calendar candidate) {
1028 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1029 }
1030
1031
1036 protected boolean matchesIndividualByDay(
1037 Calendar candidate, DayAndPosition pos) {
1038
1039 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1040 return false;
1041 }
1042
1043 int position = pos.getDayPosition();
1044
1045 if (position == 0) {
1046 return true;
1047 }
1048
1049 int field = Calendar.DAY_OF_MONTH;
1050
1051 if (position > 0) {
1052 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1053
1054 return (position == candidatePosition);
1055 }
1056 else {
1057
1058
1059
1060 int negativeCandidatePosition =
1061 ((candidate.getActualMaximum(field) - candidate.get(field)) /
1062 7) + 1;
1063
1064 return (-position == negativeCandidatePosition);
1065 }
1066 }
1067
1068
1073 protected String stringizeIntArray(int[] a) {
1074 if (a == null) {
1075 return "null";
1076 }
1077
1078 StringBundler sb = new StringBundler(2 * a.length + 1);
1079
1080 sb.append("[");
1081
1082 for (int i = 0; i < a.length; i++) {
1083 if (i != 0) {
1084 sb.append(",");
1085 }
1086
1087 sb.append(a[i]);
1088 }
1089
1090 sb.append("]");
1091
1092 return sb.toString();
1093 }
1094
1095
1098 protected DayAndPosition[] byDay;
1099
1100
1103 protected int[] byMonth;
1104
1105
1108 protected int[] byMonthDay;
1109
1110
1113 protected int[] byWeekNo;
1114
1115
1118 protected int[] byYearDay;
1119
1120
1123 protected Calendar dtStart;
1124
1125
1128 protected Duration duration;
1129
1130
1133 protected int frequency;
1134
1135
1138 protected int interval;
1139
1140
1143 protected int occurrence = 0;
1144
1145
1148 protected Calendar until;
1149
1150 }