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
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
353 public boolean isInRecurrence(Calendar current) {
354 return isInRecurrence(current, false);
355 }
356
357
364 public boolean isInRecurrence(Calendar current, boolean debug) {
365 Calendar myCurrent = (Calendar)current.clone();
366
367
368
369 myCurrent.clear(Calendar.ZONE_OFFSET);
370 myCurrent.clear(Calendar.DST_OFFSET);
371 myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
372 myCurrent.setMinimalDaysInFirstWeek(4);
373 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
374 myCurrent.set(Calendar.SECOND, 0);
375 myCurrent.set(Calendar.MILLISECOND, 0);
376
377 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
378
379
380
381 if (debug) {
382 System.err.println("current < start");
383 }
384
385 return false;
386 }
387
388 Calendar candidate = getCandidateStartTime(myCurrent);
389
390
391
392 while ((candidate.getTime().getTime() + duration.getInterval()) >
393 myCurrent.getTime().getTime()) {
394
395 if (candidateIsInRecurrence(candidate, debug)) {
396 return true;
397 }
398
399
400
401 candidate.add(Calendar.SECOND, -1);
402
403
404
405 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
406 if (debug) {
407 System.err.println("No candidates after dtStart");
408 }
409
410 return false;
411 }
412
413 candidate = getCandidateStartTime(candidate);
414 }
415
416 if (debug) {
417 System.err.println("No matching candidates");
418 }
419
420 return false;
421 }
422
423
426 public void setByDay(DayAndPosition[] b) {
427 if (b == null) {
428 byDay = null;
429
430 return;
431 }
432
433 byDay = new DayAndPosition[b.length];
434
435
439 for (int i = 0; i < b.length; i++) {
440 byDay[i] = (DayAndPosition)b[i].clone();
441 }
442 }
443
444
447 public void setByMonth(int[] b) {
448 if (b == null) {
449 byMonth = null;
450
451 return;
452 }
453
454 byMonth = new int[b.length];
455
456 System.arraycopy(b, 0, byMonth, 0, b.length);
457 }
458
459
462 public void setByMonthDay(int[] b) {
463 if (b == null) {
464 byMonthDay = null;
465
466 return;
467 }
468
469 byMonthDay = new int[b.length];
470
471 System.arraycopy(b, 0, byMonthDay, 0, b.length);
472 }
473
474
477 public void setByWeekNo(int[] b) {
478 if (b == null) {
479 byWeekNo = null;
480
481 return;
482 }
483
484 byWeekNo = new int[b.length];
485
486 System.arraycopy(b, 0, byWeekNo, 0, b.length);
487 }
488
489
492 public void setByYearDay(int[] b) {
493 if (b == null) {
494 byYearDay = null;
495
496 return;
497 }
498
499 byYearDay = new int[b.length];
500
501 System.arraycopy(b, 0, byYearDay, 0, b.length);
502 }
503
504
507 public void setDtEnd(Calendar end) {
508 Calendar tempEnd = (Calendar)end.clone();
509
510 tempEnd.clear(Calendar.ZONE_OFFSET);
511 tempEnd.clear(Calendar.DST_OFFSET);
512 tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
513 duration.setInterval(
514 tempEnd.getTime().getTime() - dtStart.getTime().getTime());
515 }
516
517
520 public void setDtStart(Calendar start) {
521 int oldStart;
522
523 if (dtStart != null) {
524 oldStart = dtStart.getFirstDayOfWeek();
525 }
526 else {
527 oldStart = Calendar.MONDAY;
528 }
529
530 if (start == null) {
531 dtStart = CalendarFactoryUtil.getCalendar(
532 TimeZoneUtil.getTimeZone(StringPool.UTC));
533
534 dtStart.setTime(new Date(0L));
535 }
536 else {
537 dtStart = (Calendar)start.clone();
538
539 dtStart.clear(Calendar.ZONE_OFFSET);
540 dtStart.clear(Calendar.DST_OFFSET);
541 dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
542 }
543
544 dtStart.setMinimalDaysInFirstWeek(4);
545 dtStart.setFirstDayOfWeek(oldStart);
546 dtStart.set(Calendar.SECOND, 0);
547 dtStart.set(Calendar.MILLISECOND, 0);
548 }
549
550
553 public void setDuration(Duration d) {
554 duration = (Duration)d.clone();
555 }
556
557
560 public void setFrequency(int freq) {
561 if ((frequency != DAILY) && (frequency != WEEKLY) &&
562 (frequency != MONTHLY) && (frequency != YEARLY) &&
563 (frequency != NO_RECURRENCE)) {
564
565 throw new IllegalArgumentException("Invalid frequency");
566 }
567
568 frequency = freq;
569 }
570
571
574 public void setInterval(int intr) {
575 interval = (intr > 0) ? intr : 1;
576 }
577
578
581 public void setOccurrence(int occur) {
582 occurrence = occur;
583 }
584
585
588 public void setUntil(Calendar u) {
589 if (u == null) {
590 until = null;
591
592 return;
593 }
594
595 until = (Calendar)u.clone();
596
597 until.clear(Calendar.ZONE_OFFSET);
598 until.clear(Calendar.DST_OFFSET);
599 until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
600 }
601
602
605 public void setWeekStart(int weekstart) {
606 dtStart.setFirstDayOfWeek(weekstart);
607 }
608
609
614 @Override
615 public String toString() {
616 StringBundler sb = new StringBundler();
617
618 sb.append(getClass().getName());
619 sb.append("[dtStart=");
620 sb.append((dtStart != null) ? dtStart.toString() : "null");
621 sb.append(",duration=");
622 sb.append((duration != null) ? duration.toString() : "null");
623 sb.append(",frequency=");
624 sb.append(frequency);
625 sb.append(",interval=");
626 sb.append(interval);
627 sb.append(",until=");
628 sb.append((until != null) ? until.toString() : "null");
629 sb.append(",byDay=");
630
631 if (byDay == null) {
632 sb.append("null");
633 }
634 else {
635 sb.append("[");
636
637 for (int i = 0; i < byDay.length; i++) {
638 if (i != 0) {
639 sb.append(",");
640 }
641
642 if (byDay[i] != null) {
643 sb.append(byDay[i].toString());
644 }
645 else {
646 sb.append("null");
647 }
648 }
649
650 sb.append("]");
651 }
652
653 sb.append(",byMonthDay=");
654 sb.append(stringizeIntArray(byMonthDay));
655 sb.append(",byYearDay=");
656 sb.append(stringizeIntArray(byYearDay));
657 sb.append(",byWeekNo=");
658 sb.append(stringizeIntArray(byWeekNo));
659 sb.append(",byMonth=");
660 sb.append(stringizeIntArray(byMonth));
661 sb.append(']');
662
663 return sb.toString();
664 }
665
666
671 protected static long getDayNumber(Calendar cal) {
672 Calendar tempCal = (Calendar)cal.clone();
673
674
675
676 tempCal.set(Calendar.MILLISECOND, 0);
677 tempCal.set(Calendar.SECOND, 0);
678 tempCal.set(Calendar.MINUTE, 0);
679 tempCal.set(Calendar.HOUR_OF_DAY, 0);
680
681 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
682 }
683
684
689 protected static long getMonthNumber(Calendar cal) {
690 return
691 ((cal.get(Calendar.YEAR) - 1970) * 12L) +
692 ((cal.get(Calendar.MONTH) - Calendar.JANUARY));
693 }
694
695
700 protected static long getWeekNumber(Calendar cal) {
701 Calendar tempCal = (Calendar)cal.clone();
702
703
704
705 tempCal.set(Calendar.MILLISECOND, 0);
706 tempCal.set(Calendar.SECOND, 0);
707 tempCal.set(Calendar.MINUTE, 0);
708 tempCal.set(Calendar.HOUR_OF_DAY, 0);
709
710
711
712 int delta =
713 tempCal.getFirstDayOfWeek() - tempCal.get(Calendar.DAY_OF_WEEK);
714
715 if (delta > 0) {
716 delta -= 7;
717 }
718
719
720
721
722
723
724 long weekEpoch =
725 (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L * 60 * 60 *
726 1000;
727
728 return
729 (tempCal.getTime().getTime() - weekEpoch) /
730 (7 * 24 * 60 * 60 * 1000);
731 }
732
733
736 protected static void reduce_constant_length_field(
737 int field, Calendar start, Calendar candidate) {
738
739 if ((start.getMaximum(field) != start.getLeastMaximum(field)) ||
740 (start.getMinimum(field) != start.getGreatestMinimum(field))) {
741
742 throw new IllegalArgumentException("Not a constant length field");
743 }
744
745 int fieldLength =
746 (start.getMaximum(field) - start.getMinimum(field) + 1);
747 int delta = start.get(field) - candidate.get(field);
748
749 if (delta > 0) {
750 delta -= fieldLength;
751 }
752
753 candidate.add(field, delta);
754 }
755
756
759 protected static void reduce_day_of_month(
760 Calendar start, Calendar candidate) {
761
762 Calendar tempCal = (Calendar)candidate.clone();
763
764 tempCal.add(Calendar.MONTH, -1);
765
766 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
767
768 if (delta > 0) {
769 delta -= tempCal.getActualMaximum(Calendar.DATE);
770 }
771
772 candidate.add(Calendar.DATE, delta);
773
774 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
775 tempCal.add(Calendar.MONTH, -1);
776 candidate.add(
777 Calendar.DATE, -tempCal.getActualMaximum(Calendar.DATE));
778 }
779 }
780
781
784 protected static void reduce_day_of_year(
785 Calendar start, Calendar candidate) {
786
787 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH)) ||
788 ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH)) &&
789 (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
790
791 candidate.add(Calendar.YEAR, -1);
792 }
793
794
795
796 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
797 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
798
799 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH)) ||
800 (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
801
802 candidate.add(Calendar.YEAR, -1);
803 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
804 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
805 }
806 }
807
808
813 protected boolean candidateIsInRecurrence(
814 Calendar candidate, boolean debug) {
815
816 if ((until != null) &&
817 (candidate.getTime().getTime() > until.getTime().getTime())) {
818
819
820
821 if (debug) {
822 System.err.println("after until");
823 }
824
825 return false;
826 }
827
828 if ((getRecurrenceCount(candidate) % interval) != 0) {
829
830
831
832 if (debug) {
833 System.err.println("not an interval rep");
834 }
835
836 return false;
837 }
838 else if ((occurrence > 0) &&
839 (getRecurrenceCount(candidate) >= occurrence)) {
840
841 return false;
842 }
843
844 if (!matchesByDay(candidate) || !matchesByMonthDay(candidate) ||
845 !matchesByYearDay(candidate) || !matchesByWeekNo(candidate) ||
846 !matchesByMonth(candidate)) {
847
848
849
850 if (debug) {
851 System.err.println("doesn't match a by*");
852 }
853
854 return false;
855 }
856
857 if (debug) {
858 System.err.println("All checks succeeded");
859 }
860
861 return true;
862 }
863
864
869 protected int getMinimumInterval() {
870 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null) ||
871 (byYearDay != null)) {
872
873 return DAILY;
874 }
875 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
876 return WEEKLY;
877 }
878 else if ((frequency == MONTHLY) || (byMonth != null)) {
879 return MONTHLY;
880 }
881 else if (frequency == YEARLY) {
882 return YEARLY;
883 }
884 else if (frequency == NO_RECURRENCE) {
885 return NO_RECURRENCE;
886 }
887 else {
888
889
890
891 throw new IllegalStateException(
892 "Internal error: Unknown frequency value");
893 }
894 }
895
896
901 protected int getRecurrenceCount(Calendar candidate) {
902 switch (frequency) {
903
904 case NO_RECURRENCE :
905 return 0;
906
907 case DAILY :
908 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
909
910 case WEEKLY :
911 Calendar tempCand = (Calendar)candidate.clone();
912
913 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
914
915 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
916
917 case MONTHLY :
918 return
919 (int)(getMonthNumber(candidate) - getMonthNumber(dtStart));
920
921 case YEARLY :
922 return
923 candidate.get(Calendar.YEAR) - dtStart.get(Calendar.YEAR);
924
925 default :
926 throw new IllegalStateException("bad frequency internally...");
927 }
928 }
929
930
935 protected boolean matchesByDay(Calendar candidate) {
936 if ((byDay == null) || (byDay.length == 0)) {
937
938
939
940 return true;
941 }
942
943 int i;
944
945 for (i = 0; i < byDay.length; i++) {
946 if (matchesIndividualByDay(candidate, byDay[i])) {
947 return true;
948 }
949 }
950
951 return false;
952 }
953
954
959 protected boolean matchesByField(
960 int[] array, int field, Calendar candidate, boolean allowNegative) {
961
962 if ((array == null) || (array.length == 0)) {
963
964
965
966 return true;
967 }
968
969 int i;
970
971 for (i = 0; i < array.length; i++) {
972 int val;
973
974 if (allowNegative && (array[i] < 0)) {
975
976
977
978 int max = candidate.getActualMaximum(field);
979
980 val = (max + 1) + array[i];
981 }
982 else {
983 val = array[i];
984 }
985
986 if (val == candidate.get(field)) {
987 return true;
988 }
989 }
990
991 return false;
992 }
993
994
999 protected boolean matchesByMonth(Calendar candidate) {
1000 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1001 }
1002
1003
1008 protected boolean matchesByMonthDay(Calendar candidate) {
1009 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1010 }
1011
1012
1017 protected boolean matchesByWeekNo(Calendar candidate) {
1018 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1019 }
1020
1021
1026 protected boolean matchesByYearDay(Calendar candidate) {
1027 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1028 }
1029
1030
1035 protected boolean matchesIndividualByDay(
1036 Calendar candidate, DayAndPosition pos) {
1037
1038 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1039 return false;
1040 }
1041
1042 int position = pos.getDayPosition();
1043
1044 if (position == 0) {
1045 return true;
1046 }
1047
1048 int field = Calendar.DAY_OF_MONTH;
1049
1050 if (position > 0) {
1051 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1052
1053 return (position == candidatePosition);
1054 }
1055 else {
1056
1057
1058
1059 int negativeCandidatePosition =
1060 ((candidate.getActualMaximum(field) - candidate.get(field)) /
1061 7) + 1;
1062
1063 return (-position == negativeCandidatePosition);
1064 }
1065 }
1066
1067
1072 protected String stringizeIntArray(int[] a) {
1073 if (a == null) {
1074 return "null";
1075 }
1076
1077 StringBundler sb = new StringBundler(2 * a.length + 1);
1078
1079 sb.append("[");
1080
1081 for (int i = 0; i < a.length; i++) {
1082 if (i != 0) {
1083 sb.append(",");
1084 }
1085
1086 sb.append(a[i]);
1087 }
1088
1089 sb.append("]");
1090
1091 return sb.toString();
1092 }
1093
1094
1097 protected DayAndPosition[] byDay;
1098
1099
1102 protected int[] byMonth;
1103
1104
1107 protected int[] byMonthDay;
1108
1109
1112 protected int[] byWeekNo;
1113
1114
1117 protected int[] byYearDay;
1118
1119
1122 protected Calendar dtStart;
1123
1124
1127 protected Duration duration;
1128
1129
1132 protected int frequency;
1133
1134
1137 protected int interval;
1138
1139
1142 protected int occurrence = 0;
1143
1144
1147 protected Calendar until;
1148
1149 }