001
014
015
044
045 package com.liferay.portal.kernel.cal;
046
047 import com.liferay.portal.kernel.util.CalendarFactoryUtil;
048 import com.liferay.portal.kernel.util.StringBundler;
049 import com.liferay.portal.kernel.util.StringPool;
050 import com.liferay.portal.kernel.util.TimeZoneUtil;
051
052 import java.io.Serializable;
053
054 import java.util.Calendar;
055 import java.util.Date;
056
057
060 public class Recurrence implements Serializable {
061
062
065 public static final int DAILY = 3;
066
067
070 public static final int MONTHLY = 5;
071
072
075 public static final int NO_RECURRENCE = 7;
076
077
080 public static final int WEEKLY = 4;
081
082
085 public static final int YEARLY = 6;
086
087
090 public Recurrence() {
091 this(null, new Duration(), NO_RECURRENCE);
092 }
093
094
097 public Recurrence(Calendar start, Duration dur) {
098 this(start, dur, NO_RECURRENCE);
099 }
100
101
104 public Recurrence(Calendar start, Duration dur, int freq) {
105 setDtStart(start);
106
107 duration = (Duration)dur.clone();
108 frequency = freq;
109 interval = 1;
110 }
111
112
113
114
119 public DayAndPosition[] getByDay() {
120 if (byDay == null) {
121 return null;
122 }
123
124 DayAndPosition[] b = new DayAndPosition[byDay.length];
125
126
130 for (int i = 0; i < byDay.length; i++) {
131 b[i] = (DayAndPosition)byDay[i].clone();
132 }
133
134 return b;
135 }
136
137
142 public int[] getByMonth() {
143 if (byMonth == null) {
144 return null;
145 }
146
147 int[] b = new int[byMonth.length];
148
149 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
150
151 return b;
152 }
153
154
159 public int[] getByMonthDay() {
160 if (byMonthDay == null) {
161 return null;
162 }
163
164 int[] b = new int[byMonthDay.length];
165
166 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
167
168 return b;
169 }
170
171
176 public int[] getByWeekNo() {
177 if (byWeekNo == null) {
178 return null;
179 }
180
181 int[] b = new int[byWeekNo.length];
182
183 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
184
185 return b;
186 }
187
188
193 public int[] getByYearDay() {
194 if (byYearDay == null) {
195 return null;
196 }
197
198 int[] b = new int[byYearDay.length];
199
200 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
201
202 return b;
203 }
204
205
210 public Calendar getCandidateStartTime(Calendar current) {
211 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
212 throw new IllegalArgumentException("Current time before DtStart");
213 }
214
215 int minInterval = getMinimumInterval();
216 Calendar candidate = (Calendar)current.clone();
217
218 if (true) {
219
220
221
222 candidate.clear(Calendar.ZONE_OFFSET);
223 candidate.clear(Calendar.DST_OFFSET);
224 candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
225 candidate.setMinimalDaysInFirstWeek(4);
226 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
227 }
228
229 if (frequency == NO_RECURRENCE) {
230 candidate.setTime(dtStart.getTime());
231
232 return candidate;
233 }
234
235 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
236 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
237 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
238
239 switch (minInterval) {
240
241 case DAILY :
242
243
244
245 break;
246
247 case WEEKLY :
248 reduce_constant_length_field(
249 Calendar.DAY_OF_WEEK, dtStart, candidate);
250 break;
251
252 case MONTHLY :
253 reduce_day_of_month(dtStart, candidate);
254 break;
255
256 case YEARLY :
257 reduce_day_of_year(dtStart, candidate);
258 break;
259 }
260
261 return candidate;
262 }
263
264
269 public Calendar getDtEnd() {
270
271
275 Calendar tempEnd = (Calendar)dtStart.clone();
276
277 tempEnd.setTime(
278 new Date(dtStart.getTime().getTime() + duration.getInterval()));
279
280 return tempEnd;
281 }
282
283
288 public Calendar getDtStart() {
289 return (Calendar)dtStart.clone();
290 }
291
292
297 public Duration getDuration() {
298 return (Duration)duration.clone();
299 }
300
301
306 public int getFrequency() {
307 return frequency;
308 }
309
310
315 public int getInterval() {
316 return interval;
317 }
318
319
324 public int getOccurrence() {
325 return occurrence;
326 }
327
328
333 public Calendar getUntil() {
334 return ((until != null) ? (Calendar)until.clone() : null);
335 }
336
337
342 public int getWeekStart() {
343 return dtStart.getFirstDayOfWeek();
344 }
345
346
351 public boolean isInRecurrence(Calendar current) {
352 return isInRecurrence(current, false);
353 }
354
355
360 public boolean isInRecurrence(Calendar current, boolean debug) {
361 Calendar myCurrent = (Calendar)current.clone();
362
363
364
365 myCurrent.clear(Calendar.ZONE_OFFSET);
366 myCurrent.clear(Calendar.DST_OFFSET);
367 myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
368 myCurrent.setMinimalDaysInFirstWeek(4);
369 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
370 myCurrent.set(Calendar.SECOND, 0);
371 myCurrent.set(Calendar.MILLISECOND, 0);
372
373 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
374
375
376
377 if (debug) {
378 System.err.println("current < start");
379 }
380
381 return false;
382 }
383
384 Calendar candidate = getCandidateStartTime(myCurrent);
385
386
387
388 while ((candidate.getTime().getTime() + duration.getInterval()) >
389 myCurrent.getTime().getTime()) {
390
391 if (candidateIsInRecurrence(candidate, debug)) {
392 return true;
393 }
394
395
396
397 candidate.add(Calendar.SECOND, -1);
398
399
400
401 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
402 if (debug) {
403 System.err.println("No candidates after dtStart");
404 }
405
406 return false;
407 }
408
409 candidate = getCandidateStartTime(candidate);
410 }
411
412 if (debug) {
413 System.err.println("No matching candidates");
414 }
415
416 return false;
417 }
418
419
422 public void setByDay(DayAndPosition[] b) {
423 if (b == null) {
424 byDay = null;
425
426 return;
427 }
428
429 byDay = new DayAndPosition[b.length];
430
431
435 for (int i = 0; i < b.length; i++) {
436 byDay[i] = (DayAndPosition)b[i].clone();
437 }
438 }
439
440
443 public void setByMonth(int[] b) {
444 if (b == null) {
445 byMonth = null;
446
447 return;
448 }
449
450 byMonth = new int[b.length];
451
452 System.arraycopy(b, 0, byMonth, 0, b.length);
453 }
454
455
458 public void setByMonthDay(int[] b) {
459 if (b == null) {
460 byMonthDay = null;
461
462 return;
463 }
464
465 byMonthDay = new int[b.length];
466
467 System.arraycopy(b, 0, byMonthDay, 0, b.length);
468 }
469
470
473 public void setByWeekNo(int[] b) {
474 if (b == null) {
475 byWeekNo = null;
476
477 return;
478 }
479
480 byWeekNo = new int[b.length];
481
482 System.arraycopy(b, 0, byWeekNo, 0, b.length);
483 }
484
485
488 public void setByYearDay(int[] b) {
489 if (b == null) {
490 byYearDay = null;
491
492 return;
493 }
494
495 byYearDay = new int[b.length];
496
497 System.arraycopy(b, 0, byYearDay, 0, b.length);
498 }
499
500
503 public void setDtEnd(Calendar end) {
504 Calendar tempEnd = (Calendar)end.clone();
505
506 tempEnd.clear(Calendar.ZONE_OFFSET);
507 tempEnd.clear(Calendar.DST_OFFSET);
508 tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
509 duration.setInterval(
510 tempEnd.getTime().getTime() - dtStart.getTime().getTime());
511 }
512
513
516 public void setDtStart(Calendar start) {
517 int oldStart;
518
519 if (dtStart != null) {
520 oldStart = dtStart.getFirstDayOfWeek();
521 }
522 else {
523 oldStart = Calendar.MONDAY;
524 }
525
526 if (start == null) {
527 dtStart = CalendarFactoryUtil.getCalendar(
528 TimeZoneUtil.getTimeZone(StringPool.UTC));
529
530 dtStart.setTime(new Date(0L));
531 }
532 else {
533 dtStart = (Calendar)start.clone();
534
535 dtStart.clear(Calendar.ZONE_OFFSET);
536 dtStart.clear(Calendar.DST_OFFSET);
537 dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
538 }
539
540 dtStart.setMinimalDaysInFirstWeek(4);
541 dtStart.setFirstDayOfWeek(oldStart);
542 dtStart.set(Calendar.SECOND, 0);
543 dtStart.set(Calendar.MILLISECOND, 0);
544 }
545
546
549 public void setDuration(Duration d) {
550 duration = (Duration)d.clone();
551 }
552
553
556 public void setFrequency(int freq) {
557 if ((frequency != DAILY) && (frequency != WEEKLY) &&
558 (frequency != MONTHLY) && (frequency != YEARLY) &&
559 (frequency != NO_RECURRENCE)) {
560
561 throw new IllegalArgumentException("Invalid frequency");
562 }
563
564 frequency = freq;
565 }
566
567
570 public void setInterval(int intr) {
571 interval = (intr > 0) ? intr : 1;
572 }
573
574
577 public void setOccurrence(int occur) {
578 occurrence = occur;
579 }
580
581
584 public void setUntil(Calendar u) {
585 if (u == null) {
586 until = null;
587
588 return;
589 }
590
591 until = (Calendar)u.clone();
592
593 until.clear(Calendar.ZONE_OFFSET);
594 until.clear(Calendar.DST_OFFSET);
595 until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
596 }
597
598
601 public void setWeekStart(int weekstart) {
602 dtStart.setFirstDayOfWeek(weekstart);
603 }
604
605
610 @Override
611 public String toString() {
612 StringBundler sb = new StringBundler();
613
614 sb.append(getClass().getName());
615 sb.append("[dtStart=");
616 sb.append((dtStart != null) ? dtStart.toString() : "null");
617 sb.append(",duration=");
618 sb.append((duration != null) ? duration.toString() : "null");
619 sb.append(",frequency=");
620 sb.append(frequency);
621 sb.append(",interval=");
622 sb.append(interval);
623 sb.append(",until=");
624 sb.append((until != null) ? until.toString() : "null");
625 sb.append(",byDay=");
626
627 if (byDay == null) {
628 sb.append("null");
629 }
630 else {
631 sb.append("[");
632
633 for (int i = 0; i < byDay.length; i++) {
634 if (i != 0) {
635 sb.append(",");
636 }
637
638 if (byDay[i] != null) {
639 sb.append(byDay[i].toString());
640 }
641 else {
642 sb.append("null");
643 }
644 }
645
646 sb.append("]");
647 }
648
649 sb.append(",byMonthDay=");
650 sb.append(stringizeIntArray(byMonthDay));
651 sb.append(",byYearDay=");
652 sb.append(stringizeIntArray(byYearDay));
653 sb.append(",byWeekNo=");
654 sb.append(stringizeIntArray(byWeekNo));
655 sb.append(",byMonth=");
656 sb.append(stringizeIntArray(byMonth));
657 sb.append(']');
658
659 return sb.toString();
660 }
661
662
667 protected static long getDayNumber(Calendar cal) {
668 Calendar tempCal = (Calendar)cal.clone();
669
670
671
672 tempCal.set(Calendar.MILLISECOND, 0);
673 tempCal.set(Calendar.SECOND, 0);
674 tempCal.set(Calendar.MINUTE, 0);
675 tempCal.set(Calendar.HOUR_OF_DAY, 0);
676
677 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
678 }
679
680
685 protected static long getMonthNumber(Calendar cal) {
686 return
687 ((cal.get(Calendar.YEAR) - 1970) * 12L) +
688 ((cal.get(Calendar.MONTH) - Calendar.JANUARY));
689 }
690
691
696 protected static long getWeekNumber(Calendar cal) {
697 Calendar tempCal = (Calendar)cal.clone();
698
699
700
701 tempCal.set(Calendar.MILLISECOND, 0);
702 tempCal.set(Calendar.SECOND, 0);
703 tempCal.set(Calendar.MINUTE, 0);
704 tempCal.set(Calendar.HOUR_OF_DAY, 0);
705
706
707
708 int delta =
709 tempCal.getFirstDayOfWeek() - tempCal.get(Calendar.DAY_OF_WEEK);
710
711 if (delta > 0) {
712 delta -= 7;
713 }
714
715
716
717
718
719
720 long weekEpoch =
721 (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L * 60 * 60 *
722 1000;
723
724 return
725 (tempCal.getTime().getTime() - weekEpoch) /
726 (7 * 24 * 60 * 60 * 1000);
727 }
728
729
732 protected static void reduce_constant_length_field(
733 int field, Calendar start, Calendar candidate) {
734
735 if ((start.getMaximum(field) != start.getLeastMaximum(field)) ||
736 (start.getMinimum(field) != start.getGreatestMinimum(field))) {
737
738 throw new IllegalArgumentException("Not a constant length field");
739 }
740
741 int fieldLength =
742 (start.getMaximum(field) - start.getMinimum(field) + 1);
743 int delta = start.get(field) - candidate.get(field);
744
745 if (delta > 0) {
746 delta -= fieldLength;
747 }
748
749 candidate.add(field, delta);
750 }
751
752
755 protected static void reduce_day_of_month(
756 Calendar start, Calendar candidate) {
757
758 Calendar tempCal = (Calendar)candidate.clone();
759
760 tempCal.add(Calendar.MONTH, -1);
761
762 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
763
764 if (delta > 0) {
765 delta -= tempCal.getActualMaximum(Calendar.DATE);
766 }
767
768 candidate.add(Calendar.DATE, delta);
769
770 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
771 tempCal.add(Calendar.MONTH, -1);
772 candidate.add(
773 Calendar.DATE, -tempCal.getActualMaximum(Calendar.DATE));
774 }
775 }
776
777
780 protected static void reduce_day_of_year(
781 Calendar start, Calendar candidate) {
782
783 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH)) ||
784 ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH)) &&
785 (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
786
787 candidate.add(Calendar.YEAR, -1);
788 }
789
790
791
792 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
793 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
794
795 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH)) ||
796 (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
797
798 candidate.add(Calendar.YEAR, -1);
799 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
800 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
801 }
802 }
803
804
809 protected boolean candidateIsInRecurrence(
810 Calendar candidate, boolean debug) {
811
812 if ((until != null) &&
813 (candidate.getTime().getTime() > until.getTime().getTime())) {
814
815
816
817 if (debug) {
818 System.err.println("after until");
819 }
820
821 return false;
822 }
823
824 if ((getRecurrenceCount(candidate) % interval) != 0) {
825
826
827
828 if (debug) {
829 System.err.println("not an interval rep");
830 }
831
832 return false;
833 }
834 else if ((occurrence > 0) &&
835 (getRecurrenceCount(candidate) >= occurrence)) {
836
837 return false;
838 }
839
840 if (!matchesByDay(candidate) || !matchesByMonthDay(candidate) ||
841 !matchesByYearDay(candidate) || !matchesByWeekNo(candidate) ||
842 !matchesByMonth(candidate)) {
843
844
845
846 if (debug) {
847 System.err.println("doesn't match a by*");
848 }
849
850 return false;
851 }
852
853 if (debug) {
854 System.err.println("All checks succeeded");
855 }
856
857 return true;
858 }
859
860
865 protected int getMinimumInterval() {
866 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null) ||
867 (byYearDay != null)) {
868
869 return DAILY;
870 }
871 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
872 return WEEKLY;
873 }
874 else if ((frequency == MONTHLY) || (byMonth != null)) {
875 return MONTHLY;
876 }
877 else if (frequency == YEARLY) {
878 return YEARLY;
879 }
880 else if (frequency == NO_RECURRENCE) {
881 return NO_RECURRENCE;
882 }
883 else {
884
885
886
887 throw new IllegalStateException(
888 "Internal error: Unknown frequency value");
889 }
890 }
891
892
897 protected int getRecurrenceCount(Calendar candidate) {
898 switch (frequency) {
899
900 case NO_RECURRENCE :
901 return 0;
902
903 case DAILY :
904 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
905
906 case WEEKLY :
907 Calendar tempCand = (Calendar)candidate.clone();
908
909 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
910
911 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
912
913 case MONTHLY :
914 return
915 (int)(getMonthNumber(candidate) - getMonthNumber(dtStart));
916
917 case YEARLY :
918 return
919 candidate.get(Calendar.YEAR) - dtStart.get(Calendar.YEAR);
920
921 default :
922 throw new IllegalStateException("bad frequency internally...");
923 }
924 }
925
926
931 protected boolean matchesByDay(Calendar candidate) {
932 if ((byDay == null) || (byDay.length == 0)) {
933
934
935
936 return true;
937 }
938
939 int i;
940
941 for (i = 0; i < byDay.length; i++) {
942 if (matchesIndividualByDay(candidate, byDay[i])) {
943 return true;
944 }
945 }
946
947 return false;
948 }
949
950
955 protected boolean matchesByField(
956 int[] array, int field, Calendar candidate, boolean allowNegative) {
957
958 if ((array == null) || (array.length == 0)) {
959
960
961
962 return true;
963 }
964
965 int i;
966
967 for (i = 0; i < array.length; i++) {
968 int val;
969
970 if (allowNegative && (array[i] < 0)) {
971
972
973
974 int max = candidate.getActualMaximum(field);
975
976 val = (max + 1) + array[i];
977 }
978 else {
979 val = array[i];
980 }
981
982 if (val == candidate.get(field)) {
983 return true;
984 }
985 }
986
987 return false;
988 }
989
990
995 protected boolean matchesByMonth(Calendar candidate) {
996 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
997 }
998
999
1004 protected boolean matchesByMonthDay(Calendar candidate) {
1005 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1006 }
1007
1008
1013 protected boolean matchesByWeekNo(Calendar candidate) {
1014 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1015 }
1016
1017
1022 protected boolean matchesByYearDay(Calendar candidate) {
1023 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1024 }
1025
1026
1031 protected boolean matchesIndividualByDay(
1032 Calendar candidate, DayAndPosition pos) {
1033
1034 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1035 return false;
1036 }
1037
1038 int position = pos.getDayPosition();
1039
1040 if (position == 0) {
1041 return true;
1042 }
1043
1044 int field = Calendar.DAY_OF_MONTH;
1045
1046 if (position > 0) {
1047 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1048
1049 return (position == candidatePosition);
1050 }
1051 else {
1052
1053
1054
1055 int negativeCandidatePosition =
1056 ((candidate.getActualMaximum(field) - candidate.get(field)) /
1057 7) + 1;
1058
1059 return (-position == negativeCandidatePosition);
1060 }
1061 }
1062
1063
1068 protected String stringizeIntArray(int[] a) {
1069 if (a == null) {
1070 return "null";
1071 }
1072
1073 StringBundler sb = new StringBundler(2 * a.length + 1);
1074
1075 sb.append("[");
1076
1077 for (int i = 0; i < a.length; i++) {
1078 if (i != 0) {
1079 sb.append(",");
1080 }
1081
1082 sb.append(a[i]);
1083 }
1084
1085 sb.append("]");
1086
1087 return sb.toString();
1088 }
1089
1090
1093 protected DayAndPosition[] byDay;
1094
1095
1098 protected int[] byMonth;
1099
1100
1103 protected int[] byMonthDay;
1104
1105
1108 protected int[] byWeekNo;
1109
1110
1113 protected int[] byYearDay;
1114
1115
1118 protected Calendar dtStart;
1119
1120
1123 protected Duration duration;
1124
1125
1128 protected int frequency;
1129
1130
1133 protected int interval;
1134
1135
1138 protected int occurrence = 0;
1139
1140
1143 protected Calendar until;
1144
1145 }