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