001
014
015
044
045 package com.liferay.util.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
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
211 public Calendar getCandidateStartTime(Calendar current) {
212 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
213 throw new IllegalArgumentException("Current time before DtStart");
214 }
215
216 int minInterval = getMinimumInterval();
217 Calendar candidate = (Calendar)current.clone();
218
219 if (true) {
220
221
222
223 candidate.clear(Calendar.ZONE_OFFSET);
224 candidate.clear(Calendar.DST_OFFSET);
225 candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
226 candidate.setMinimalDaysInFirstWeek(4);
227 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
228 }
229
230 if (frequency == NO_RECURRENCE) {
231 candidate.setTime(dtStart.getTime());
232
233 return candidate;
234 }
235
236 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
237 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
238 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
239
240 switch (minInterval) {
241
242 case DAILY :
243
244
245
246 break;
247
248 case WEEKLY :
249 reduce_constant_length_field(
250 Calendar.DAY_OF_WEEK, dtStart, candidate);
251 break;
252
253 case MONTHLY :
254 reduce_day_of_month(dtStart, candidate);
255 break;
256
257 case YEARLY :
258 reduce_day_of_year(dtStart, candidate);
259 break;
260 }
261
262 return candidate;
263 }
264
265
270 public Calendar getDtEnd() {
271
272
276 Calendar tempEnd = (Calendar)dtStart.clone();
277
278 tempEnd.setTime(
279 new Date(dtStart.getTime().getTime() + duration.getInterval()));
280
281 return tempEnd;
282 }
283
284
289 public Calendar getDtStart() {
290 return (Calendar)dtStart.clone();
291 }
292
293
298 public Duration getDuration() {
299 return (Duration)duration.clone();
300 }
301
302
307 public int getFrequency() {
308 return frequency;
309 }
310
311
316 public int getInterval() {
317 return interval;
318 }
319
320
325 public int getOccurrence() {
326 return occurrence;
327 }
328
329
334 public Calendar getUntil() {
335 return ((until != null) ? (Calendar)until.clone() : null);
336 }
337
338
343 public int getWeekStart() {
344 return dtStart.getFirstDayOfWeek();
345 }
346
347
352 public boolean isInRecurrence(Calendar current) {
353 return isInRecurrence(current, false);
354 }
355
356
361 public boolean isInRecurrence(Calendar current, boolean debug) {
362 Calendar myCurrent = (Calendar)current.clone();
363
364
365
366 myCurrent.clear(Calendar.ZONE_OFFSET);
367 myCurrent.clear(Calendar.DST_OFFSET);
368 myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
369 myCurrent.setMinimalDaysInFirstWeek(4);
370 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
371
372 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
373
374
375
376 if (debug) {
377 System.err.println("current < start");
378 }
379
380 return false;
381 }
382
383 Calendar candidate = getCandidateStartTime(myCurrent);
384
385
386
387 while ((candidate.getTime().getTime() + duration.getInterval()) >
388 myCurrent.getTime().getTime()) {
389
390 if (candidateIsInRecurrence(candidate, debug)) {
391 return true;
392 }
393
394
395
396 candidate.add(Calendar.SECOND, -1);
397
398
399
400 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
401 if (debug) {
402 System.err.println("No candidates after dtStart");
403 }
404
405 return false;
406 }
407
408 candidate = getCandidateStartTime(candidate);
409 }
410
411 if (debug) {
412 System.err.println("No matching candidates");
413 }
414
415 return false;
416 }
417
418
421 public void setByDay(DayAndPosition[] b) {
422 if (b == null) {
423 byDay = null;
424
425 return;
426 }
427
428 byDay = new DayAndPosition[b.length];
429
430
434 for (int i = 0; i < b.length; i++) {
435 byDay[i] = (DayAndPosition)b[i].clone();
436 }
437 }
438
439
442 public void setByMonth(int[] b) {
443 if (b == null) {
444 byMonth = null;
445
446 return;
447 }
448
449 byMonth = new int[b.length];
450
451 System.arraycopy(b, 0, byMonth, 0, b.length);
452 }
453
454
457 public void setByMonthDay(int[] b) {
458 if (b == null) {
459 byMonthDay = null;
460
461 return;
462 }
463
464 byMonthDay = new int[b.length];
465
466 System.arraycopy(b, 0, byMonthDay, 0, b.length);
467 }
468
469
472 public void setByWeekNo(int[] b) {
473 if (b == null) {
474 byWeekNo = null;
475
476 return;
477 }
478
479 byWeekNo = new int[b.length];
480
481 System.arraycopy(b, 0, byWeekNo, 0, b.length);
482 }
483
484
487 public void setByYearDay(int[] b) {
488 if (b == null) {
489 byYearDay = null;
490
491 return;
492 }
493
494 byYearDay = new int[b.length];
495
496 System.arraycopy(b, 0, byYearDay, 0, b.length);
497 }
498
499
502 public void setDtEnd(Calendar end) {
503 Calendar tempEnd = (Calendar)end.clone();
504
505 tempEnd.clear(Calendar.ZONE_OFFSET);
506 tempEnd.clear(Calendar.DST_OFFSET);
507 tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
508 duration.setInterval(
509 tempEnd.getTime().getTime() - dtStart.getTime().getTime());
510 }
511
512
515 public void setDtStart(Calendar start) {
516 int oldStart;
517
518 if (dtStart != null) {
519 oldStart = dtStart.getFirstDayOfWeek();
520 }
521 else {
522 oldStart = Calendar.MONDAY;
523 }
524
525 if (start == null) {
526 dtStart = CalendarFactoryUtil.getCalendar(
527 TimeZoneUtil.getTimeZone(StringPool.UTC));
528
529 dtStart.setTime(new Date(0L));
530 }
531 else {
532 dtStart = (Calendar)start.clone();
533
534 dtStart.clear(Calendar.ZONE_OFFSET);
535 dtStart.clear(Calendar.DST_OFFSET);
536 dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
537 }
538
539 dtStart.setMinimalDaysInFirstWeek(4);
540 dtStart.setFirstDayOfWeek(oldStart);
541 }
542
543
546 public void setDuration(Duration d) {
547 duration = (Duration)d.clone();
548 }
549
550
553 public void setFrequency(int freq) {
554 if ((frequency != DAILY) && (frequency != WEEKLY)
555 && (frequency != MONTHLY) && (frequency != YEARLY)
556 && (frequency != NO_RECURRENCE)) {
557 throw new IllegalArgumentException("Invalid frequency");
558 }
559
560 frequency = freq;
561 }
562
563
566 public void setInterval(int intr) {
567 interval = (intr > 0) ? intr : 1;
568 }
569
570
573 public void setOccurrence(int occur) {
574 occurrence = occur;
575 }
576
577
580 public void setUntil(Calendar u) {
581 if (u == null) {
582 until = null;
583
584 return;
585 }
586
587 until = (Calendar)u.clone();
588
589 until.clear(Calendar.ZONE_OFFSET);
590 until.clear(Calendar.DST_OFFSET);
591 until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
592 }
593
594
597 public void setWeekStart(int weekstart) {
598 dtStart.setFirstDayOfWeek(weekstart);
599 }
600
601
606 @Override
607 public String toString() {
608 StringBundler sb = new StringBundler();
609
610 sb.append(getClass().getName());
611 sb.append("[dtStart=");
612 sb.append((dtStart != null) ? dtStart.toString() : "null");
613 sb.append(",duration=");
614 sb.append((duration != null) ? duration.toString() : "null");
615 sb.append(",frequency=");
616 sb.append(frequency);
617 sb.append(",interval=");
618 sb.append(interval);
619 sb.append(",until=");
620 sb.append((until != null) ? until.toString() : "null");
621 sb.append(",byDay=");
622
623 if (byDay == null) {
624 sb.append("null");
625 }
626 else {
627 sb.append("[");
628
629 for (int i = 0; i < byDay.length; i++) {
630 if (i != 0) {
631 sb.append(",");
632 }
633
634 if (byDay[i] != null) {
635 sb.append(byDay[i].toString());
636 }
637 else {
638 sb.append("null");
639 }
640 }
641
642 sb.append("]");
643 }
644
645 sb.append(",byMonthDay=");
646 sb.append(stringizeIntArray(byMonthDay));
647 sb.append(",byYearDay=");
648 sb.append(stringizeIntArray(byYearDay));
649 sb.append(",byWeekNo=");
650 sb.append(stringizeIntArray(byWeekNo));
651 sb.append(",byMonth=");
652 sb.append(stringizeIntArray(byMonth));
653 sb.append(']');
654
655 return sb.toString();
656 }
657
658
663 protected static long getDayNumber(Calendar cal) {
664 Calendar tempCal = (Calendar)cal.clone();
665
666
667
668 tempCal.set(Calendar.MILLISECOND, 0);
669 tempCal.set(Calendar.SECOND, 0);
670 tempCal.set(Calendar.MINUTE, 0);
671 tempCal.set(Calendar.HOUR_OF_DAY, 0);
672
673 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
674 }
675
676
681 protected static long getMonthNumber(Calendar cal) {
682 return
683 ((cal.get(Calendar.YEAR) - 1970) * 12L) +
684 (cal.get(Calendar.MONTH) - Calendar.JANUARY);
685 }
686
687
692 protected static long getWeekNumber(Calendar cal) {
693 Calendar tempCal = (Calendar)cal.clone();
694
695
696
697 tempCal.set(Calendar.MILLISECOND, 0);
698 tempCal.set(Calendar.SECOND, 0);
699 tempCal.set(Calendar.MINUTE, 0);
700 tempCal.set(Calendar.HOUR_OF_DAY, 0);
701
702
703
704 int delta = tempCal.getFirstDayOfWeek()
705 - tempCal.get(Calendar.DAY_OF_WEEK);
706
707 if (delta > 0) {
708 delta -= 7;
709 }
710
711
712
713
714
715
716 long weekEpoch =
717 (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L * 60 * 60 *
718 1000L;
719
720 return
721 (tempCal.getTime().getTime() - weekEpoch) /
722 (7 * 24 * 60 * 60 * 1000);
723 }
724
725
728 protected static void reduce_constant_length_field(
729 int field, Calendar start, Calendar candidate) {
730
731 if ((start.getMaximum(field) != start.getLeastMaximum(field))
732 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
733 throw new IllegalArgumentException("Not a constant length field");
734 }
735
736 int fieldLength =
737 (start.getMaximum(field) - start.getMinimum(field) + 1);
738 int delta = start.get(field) - candidate.get(field);
739
740 if (delta > 0) {
741 delta -= fieldLength;
742 }
743
744 candidate.add(field, delta);
745 }
746
747
750 protected static void reduce_day_of_month(
751 Calendar start, Calendar candidate) {
752
753 Calendar tempCal = (Calendar)candidate.clone();
754
755 tempCal.add(Calendar.MONTH, -1);
756
757 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
758
759 if (delta > 0) {
760 delta -= tempCal.getActualMaximum(Calendar.DATE);
761 }
762
763 candidate.add(Calendar.DATE, delta);
764
765 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
766 tempCal.add(Calendar.MONTH, -1);
767 candidate.add(
768 Calendar.DATE, -tempCal.getActualMaximum(Calendar.DATE));
769 }
770 }
771
772
775 protected static void reduce_day_of_year(
776 Calendar start, Calendar candidate) {
777
778 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
779 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
780 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
781 candidate.add(Calendar.YEAR, -1);
782 }
783
784
785
786 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
787 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
788
789 while ((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 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
794 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
795 }
796 }
797
798
803 protected boolean candidateIsInRecurrence(
804 Calendar candidate, boolean debug) {
805
806 if ((until != null)
807 && (candidate.getTime().getTime() > until.getTime().getTime())) {
808
809
810
811 if (debug) {
812 System.err.println("after until");
813 }
814
815 return false;
816 }
817
818 if ((getRecurrenceCount(candidate) % interval) != 0) {
819
820
821
822 if (debug) {
823 System.err.println("not an interval rep");
824 }
825
826 return false;
827 }
828 else if ((occurrence > 0) &&
829 (getRecurrenceCount(candidate) >= occurrence)) {
830
831 return false;
832 }
833
834 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
835 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
836 ||!matchesByMonth(candidate)) {
837
838
839
840 if (debug) {
841 System.err.println("doesn't match a by*");
842 }
843
844 return false;
845 }
846
847 if (debug) {
848 System.err.println("All checks succeeded");
849 }
850
851 return true;
852 }
853
854
859 protected int getMinimumInterval() {
860 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
861 || (byYearDay != null)) {
862 return DAILY;
863 }
864 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
865 return WEEKLY;
866 }
867 else if ((frequency == MONTHLY) || (byMonth != null)) {
868 return MONTHLY;
869 }
870 else if (frequency == YEARLY) {
871 return YEARLY;
872 }
873 else if (frequency == NO_RECURRENCE) {
874 return NO_RECURRENCE;
875 }
876 else {
877
878
879
880 throw new IllegalStateException(
881 "Internal error: Unknown frequency value");
882 }
883 }
884
885
890 protected int getRecurrenceCount(Calendar candidate) {
891 switch (frequency) {
892
893 case NO_RECURRENCE :
894 return 0;
895
896 case DAILY :
897 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
898
899 case WEEKLY :
900 Calendar tempCand = (Calendar)candidate.clone();
901
902 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
903
904 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
905
906 case MONTHLY :
907 return
908 (int)(getMonthNumber(candidate) - getMonthNumber(dtStart));
909
910 case YEARLY :
911 return
912 candidate.get(Calendar.YEAR) - dtStart.get(Calendar.YEAR);
913
914 default :
915 throw new IllegalStateException("bad frequency internally...");
916 }
917 }
918
919
924 protected boolean matchesByDay(Calendar candidate) {
925 if ((byDay == null) || (byDay.length == 0)) {
926
927
928
929 return true;
930 }
931
932 int i;
933
934 for (i = 0; i < byDay.length; i++) {
935 if (matchesIndividualByDay(candidate, byDay[i])) {
936 return true;
937 }
938 }
939
940 return false;
941 }
942
943
948 protected boolean matchesByField(
949 int[] array, int field, Calendar candidate, boolean allowNegative) {
950
951 if ((array == null) || (array.length == 0)) {
952
953
954
955 return true;
956 }
957
958 int i;
959
960 for (i = 0; i < array.length; i++) {
961 int val;
962
963 if (allowNegative && (array[i] < 0)) {
964
965
966
967 int max = candidate.getActualMaximum(field);
968
969 val = (max + 1) + array[i];
970 }
971 else {
972 val = array[i];
973 }
974
975 if (val == candidate.get(field)) {
976 return true;
977 }
978 }
979
980 return false;
981 }
982
983
988 protected boolean matchesByMonth(Calendar candidate) {
989 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
990 }
991
992
997 protected boolean matchesByMonthDay(Calendar candidate) {
998 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
999 }
1000
1001
1006 protected boolean matchesByWeekNo(Calendar candidate) {
1007 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1008 }
1009
1010
1015 protected boolean matchesByYearDay(Calendar candidate) {
1016 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1017 }
1018
1019
1024 protected boolean matchesIndividualByDay(
1025 Calendar candidate, DayAndPosition pos) {
1026
1027 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1028 return false;
1029 }
1030
1031 int position = pos.getDayPosition();
1032
1033 if (position == 0) {
1034 return true;
1035 }
1036
1037 int field = Calendar.DAY_OF_MONTH;
1038
1039 if (position > 0) {
1040 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1041
1042 return (position == candidatePosition);
1043 }
1044 else {
1045
1046
1047
1048 int negativeCandidatePosition =
1049 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
1050 + 1;
1051
1052 return (-position == negativeCandidatePosition);
1053 }
1054 }
1055
1056
1061 protected String stringizeIntArray(int[] a) {
1062 if (a == null) {
1063 return "null";
1064 }
1065
1066 StringBundler sb = new StringBundler(2 * a.length + 1);
1067
1068 sb.append("[");
1069
1070 for (int i = 0; i < a.length; i++) {
1071 if (i != 0) {
1072 sb.append(",");
1073 }
1074
1075 sb.append(a[i]);
1076 }
1077
1078 sb.append("]");
1079
1080 return sb.toString();
1081 }
1082
1083
1086 protected DayAndPosition[] byDay;
1087
1088
1091 protected int[] byMonth;
1092
1093
1096 protected int[] byMonthDay;
1097
1098
1101 protected int[] byWeekNo;
1102
1103
1106 protected int[] byYearDay;
1107
1108
1111 protected Calendar dtStart;
1112
1113
1116 protected Duration duration;
1117
1118
1121 protected int frequency;
1122
1123
1126 protected int interval;
1127
1128
1131 protected int occurrence = 0;
1132
1133
1136 protected Calendar until;
1137
1138 }